先上网上大神整理的一张思维导图,对整体理解JVM非常有帮助

java 堆内存虚拟内存 jvm 虚拟内存_java

1、JVM内存区域

JVM内存区域就是Java虚拟机在执行Java程序的过程中把它管理的内存划分为若干个不同的数据区域,也叫运行时数据区

jdk1.8和之前的版本有所不同。

jdk1.8之前:

java 堆内存虚拟内存 jvm 虚拟内存_方法区_02


jdk1.8

java 堆内存虚拟内存 jvm 虚拟内存_方法区_03

(1)jdk1.8和jdk1.7以及之前的区别

jdk1.8和之前不同的是,jdk1.8彻底移除了方法区,取之的是元空间元空间使用的是直接内存
jdk1.7以及之前,堆内存可以分为三个部分:新生代、老生代和永生代内存。而永生代也被称为方法区。(拓展:方法区是Java虚拟机的一个概念,而永生代则是方法区的一种具体实现)。

jdk1.8之后就用元空间替代了永生代。原因有2个:
a.整个永生代有JVM本身设置的一个固定大小上限,无法调整,而元空间使用的是直接内存,只受本机可使用内存的限制,内存溢出概率大为减少。
b.元空间中存放的是类的元数据,这样加载多少类就不由MaxPermSize(方法区最大大小参数)限制了,而是由系统实际可用内存控制,可以加载更多的类。

(2)是否线程私有

线程私有:程序计数器、Java虚拟机栈、本地方法栈
线程共享:堆、方法区、直接内存(非运行时数据区的一部分)

1.1、程序计数器

程序计数器是一块比较小的内存空间,可以看作是当前线程执行的字节码的行号指示器。主要有两个作用:
(1)字节码解释器通过改变程序计数器的大小来依次读取指令,从而实现代码的流程控制,如循序执行、选择、循环和异常处理等。
(2)在多线程的情况下,程序计数器用于记录当前线程的执行位置,这样线程切换回来的时候就知道线程上次执行到什么位置。
程序计数器是唯一不会出现 OutOfMemoryError的内存区域。它的生命周期伴随线程的创建和消亡。

1.2、Java虚拟机栈

Java虚拟机栈的生命周期和线程相同,描述的是Java方法执行的内存模型,每次方法调用的数据都是通过传递的。
Java内存可以粗略的分为堆内存栈内存,其中栈内存就是Java虚拟机栈,或者是Java虚拟机栈中的局部变量表部分。(Java虚拟机栈由一个个栈帧组成,每个栈帧都有:局部变量表、操作数栈、动态链接、方法出口等

局部变量表中主要存储了编译期可知的各种数据类型long、int、boolean、byte等)和对象引用reference类型,它不同于对象本身,可能存储的是一个指向对象起始地址的引用指针,也可能是代表一个对象的句柄或其他和对象相关的位置信息)

Java虚拟机栈可能出现两种错误:
(1)StackOverFlowError:若当前Java虚拟机栈的内存不允许动态扩展,那么当线程请求的栈深度超过了Java虚拟机栈的最大深度,就是抛出这个错误。
(2)OutOfMemoryError:Java虚拟机在动态拓展栈的时候申请不到足够的内存时,会抛出这个异常。

1.3、本地方法栈

本地方法栈和Java虚拟机栈的作用相似,Java虚拟机栈是为虚拟机执行Java方法服务的,本地方法栈是为虚拟机执行本地方法(native方法)服务的

1.4、堆

堆是Java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域。几乎所有的对象实例和数组是在堆中分配内存的(是几乎不是绝对,会存在一些对象在栈上分配内存)。

Java堆也是垃圾收集器管理的主要区域,也称为GC堆。Java堆可以细分为新生代、老生代、永生代(jdk1.8后被替换为元空间)。更细致点:Eden空间、from Survivor、to Survivor空间等,进一步划分的目的是为了更好的分配和回收内存空间。

java 堆内存虚拟内存 jvm 虚拟内存_java 堆内存虚拟内存_04


对象会首先在Eden中创建,在经历一次新生代垃圾收集后,若该对象还存活,则进入到from Survivor或to Survivor,并且年龄加1,等到年龄到达一个阈值(默认是15)时,就是进入到老生代。这个阈值可以通过-XX:MaxTenuringThreshold来设置。

堆这里最容易出现的就是OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有几种,比如:
(1)OutOfMemoryError: GC Overhead Limit Exceeded: 当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
(2)java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发java.lang.OutOfMemoryError: Java heap space错误。

1.5、方法区

方法区也是被线程共享的内存区域,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译期编译的代码数据等。Java虚拟机规范中把方法区描述为堆的一个逻辑部分,但是它有一个别名Non-Heap(非堆)。
方法区也被称为永生代,在jdk1.8被移除,替换为元空间。
永生代常用参数:

-XX:PermSize=N //方法区 (永久代) 初始大小
-XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen

元空间常用参数

-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小

拓展:运行时常量池和字符串常量池
jdk1.7,运行时常量池存在方法区中,里面存的除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用),实际常量对象存在堆中的,而字符串常量池从方法区中拿到了堆中。
jdk1.8,元空间替换了方法区存于直接内存中,这时字符串常量池还在堆中,运行时常量池在直接内存的元空间中。

1.6、直接内存

直接内存不是Java虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。

JDK1.4中新加入的NIO(New Input/Output)类,引入了一种基于通道(Channel)缓存区(Buffer) 的 I/O 方式,它可以直接使用Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆Native 堆之间来回复制数据。

本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。

2、虚拟机对象

2.1、对象的创建

java 堆内存虚拟内存 jvm 虚拟内存_java_05


(1)类加载检查

Java虚拟机执行一个new指令时,首先判断这个指令能否在常量池中定位到这个类的符号引用,并且检查这个符合引用代表的类是否被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

(2)分配内存

在类加载过后,虚拟机会为新生对象分配内存,分配内存的大小在类加载的时候就已经确定了。分配内存的方式有指针碰撞空闲列表两种,取决于堆中内存是否规整,堆内存是否规整又取决于GC堆垃圾收集算法是标记-清理(不规整)还是标记-整理(规整)标记-复制也是规整的。

java 堆内存虚拟内存 jvm 虚拟内存_java_06


内存分配也有并发问题,因为系统中创建对象的操作是非常频繁的,JVM采用两种方法来保证线程安全,一是CAS+失败重试,二是先为每一个线程在Eden中分配一个内存区域TLAB,在TLAB中创建对象,若对象大小大于TLAB或者TLAB内存不够了,则采用CAS+失败重试。

(3)初始化零值

内存分配完成后,虚拟机将分配的内存空间初始化零值(不包括对象头),这样保证对象的实例字段在Java代码中不需要初始值就可以使用它们的数据类型所对应的零值。

(4)设置对象头

初始化零值后,虚拟机开始设置该对象的对象头相关信息,如该对象是哪个类的实例,类的元数据在什么地方,对象的哈希值,GC分代年龄等。

(5)执行init方法

上面的都完成后,从虚拟机的角度来看,一个对象就已经产生了,但是在Java程序来看,对象创建才开始,<init>方法还没有执行,所有字段还都为零,所以执行new命令后,需要执行<init>方法,对象按照程序员的逻辑初始化,对象才算真正的创建成功了。

2.2、对象的内存分布

Hotstop虚拟机中,对象在内存中分为三个区域:对象头,实例数据和对齐填充
对象头存储的是对象自身运行时数据(哈希值、GC分代年龄、锁标志等),还存储类型指针,即这个对象指向它的类的元数据的指针。
实例数据就是对象存储的真正有效的信息。
对齐填充不是必须存在的,没有什么特殊含义,仅仅是占位使用,因为虚拟机要求对象的大小必须是8字节的整数倍,不够的话就对齐填充。

2.3、对象的访问定位

Java程序通过虚拟机栈上的reference数据来操作对象,主要的访问方式有两种:句柄访问和直接指针

3、JVM垃圾收集

3.1、垃圾收集的区域

针对HotSpot VM的实现,它里面的GC 其实准确分类只有两大种:
(1)部分收集 (Partial GC):
新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
(2)整堆收集 (Full GC):收集整个 Java 堆和方法区。

3.2、怎么判断对象是否死亡

堆中几乎存放的所有的对象实例,垃圾回收的第一步就是判断哪些对象已经死亡。

java 堆内存虚拟内存 jvm 虚拟内存_加载_07


(1)引用计数法

给对象添加一个引用计数器,当有引用时,就加1,当引用失效后,就减1,任何时候计数器的值为0则说明这个对象是不可用的。

这个方法简单效率高,但是有个问题,不能解决两个对象之间互相循环引用的问题。

(2)可达性分析算法

通过一系列称为GC Roots的对象作为起点,从这些起点开始向下搜索,节点走过的路径称为引用链,当一个对象到达GC Roots没有任何引用链相连的话,则说明这个对象没有引用。

java 堆内存虚拟内存 jvm 虚拟内存_java 堆内存虚拟内存_08


可作为 GC Roots 的对象包括下面几种:

a.虚拟机栈(栈帧中的本地变量表)中引用的对象;

b.本地方法栈(Native 方法)中引用的对象;

c.方法区中类静态属性引用的对象;

d.方法区中常量引用的对象;

e.所有被同步锁持有的对象。

如图中,对象5、6、7到达不了GC Roots,所有是可以被回收的。

(3)不可达对象并非是非死不可

可达性分析中不可达的对象是处于缓刑阶段,要真正判断是个一个对象是否死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize方法。当对象没有覆盖finalize 方法,或 finalize方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。

被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。

(4)四种引用

强引用:大部分引用都是强引用,如果一个对象有强引用,垃圾回收器是不会回收这个对象的。

软引用:一个对象如果只具有软引用,如果内存空间足够,垃圾回收器就不会回收它,但是内存空间不够的话,就会回收这个对象。

弱引用:弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

虚引用:"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。

弱引用虚引用在程序中很少使用,软引用比较常用,因为软引用可以加速JVM对垃圾内存的回收速度,维护系统的运行安全,减少内存溢出的问题。

(5)判断废弃常量

假如在字符串常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量"abc"就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc"就会被系统清理出常量池了。

(6)判断无用的类

a.该类所有的实例都已经被回收,也就是Java 堆中不存在该类的任何实例。

b.加载该类的 ClassLoader 已经被回收。

c.该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

满足上面三个条件的类可以被回收,但是并不绝对。

3.3、垃圾收集算法

(1)标记-清除算法

首先标记出不需要回收的对象,然后清除所有没有标记的对象。该算法有两个问题,一是效率问题,二是标记-清除后会产生大量不连续的内存碎片。

java 堆内存虚拟内存 jvm 虚拟内存_加载_09


(2)标记-复制算法

为了解决效率问题,标记-复制算法把内存空间分为相等的两块,每次使用其中的一块,当这一块的内存使用完后,将存活的对象标记,复制到另一半内存中,然后清除该块已使用的内存。这样每次垃圾回收只回收一半的内存区域。

java 堆内存虚拟内存 jvm 虚拟内存_java 堆内存虚拟内存_10


(3)标记-整理算法

根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

java 堆内存虚拟内存 jvm 虚拟内存_java_11


(4)分代收集算法

根据对象存活的周期的不同,将内存分为几个区域,一般将Java堆分为新生代和老生代,这样可以根据不同年度的特点来选择不同的垃圾收集算法。

比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

3.4、垃圾收集器

垃圾收集器是垃圾收集算法的具体实现。

java 堆内存虚拟内存 jvm 虚拟内存_Java_12


java 堆内存虚拟内存 jvm 虚拟内存_Java_13

4、类加载

一个类完整的生命周期如下:

java 堆内存虚拟内存 jvm 虚拟内存_加载_14

4.1、类加载过程

类加载过程主要有三步:加载、连接、初始化。其中连接分为:验证、准备、解析

4.2、类加载器

(1)BootstrapClassLoader(启动类加载器) :最顶层的加载类,由C++实现,负责加载%JAVA_HOME%/lib目录下的jar包和类或者或被 -Xbootclasspath参数指定的路径中的所有类。
(2)ExtensionClassLoader(扩展类加载器) :主要负责加载目录 %JRE_HOME%/lib/ext 目录下的jar包和类,或被 java.ext.dirs系统变量所指定的路径下的jar包。
(3)AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。

4.3、双亲委派加载模型

每一个类都有一个对应它的类加载器。系统中的ClassLoder 在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派该父类加载器的 loadClass()处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。

java 堆内存虚拟内存 jvm 虚拟内存_加载_15


双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载(JVM区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java的核心 API不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object类。

5、JVM参数

java 堆内存虚拟内存 jvm 虚拟内存_方法区_16