JVM内存模型与GC机制

可访问个人网站进行阅读最新版本,精力有限无法多网站同步更新,更新只会在个人网站进行
介绍了下Java运行时内存划分以及GC算法、垃圾回收器。

一、Java内存区域

JVM内存模型与GC机制

JVM内存模型与GC机制

1.程序计数器:程序计数器(Program Counter Register)是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。更确切的说,一个线程的执行,是通过字节码解释器改变当前线程的计数器的值,来获取下一条需要执行的字节码指令,从而确保线程的正确执行。每个线程都有一个独立的程序计数器,是线程私有的内存

2.虚拟机栈:描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成 的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

3.本地方法栈:本地方法栈则为 JVM 使用到的 Native 方法服务。Native 方法不是以 Java 语言实现的,而是以本地语言实现的(比如 C 或 C++)。个人理解Native 方法是与操作系统直接交互的。比如通知垃圾收集器进行垃圾回收的代码 System.gc(),就是使用 native 修饰的

4.堆:堆是Java虚拟机所管理的内存中最大的一块存储区域。堆内存被所有线程共享。主要存放使用new关键字创建的对象。所有对象实例以及数组都要在堆上分配。垃圾收集器就是根据GC算法,收集堆上对象所占用的内存空间(收集的是对象占用的空间而不是对象本身)

  • Java堆分为年轻代(Young Generation)和老年代(Old Generation);年轻代又分为伊甸园(Eden)和幸存区(Survivor区);幸存区又分为From Survivor空间和 To Survivor空间。

  • 年轻代存储“新生对象”,我们新创建的对象存储在年轻代中。当年轻内存占满后,会触发Minor GC,清理年轻代内存空间。

  • 老年代存储长期存活的对象和大对象。年轻代中存储的对象,经过多次GC后仍然存活的对象会移动到老年代中进行存储。老年代空间占满后,会触发Full GC。

    注:Full GC是清理整个堆空间,包括年轻代和老年代。如果Full GC之后,堆中仍然无法存储对象,就会抛出OutOfMemoryError异常。

5.方法区:方法区同 Java 堆一样是被所有线程共享的区间,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。更具体的说,静态变量+常量+类信息(版本、方法、字段等)+运行时常量池存在方法区中。常量池是方法区的一部分,常量池中存储编译器生成的各种字面量和符号引用。字面量就是Java中常量的意思。比如文本字符串,final修饰的常量等。方法引用则包括类和接口的全限定名,方法名和描述符,字段名和描述符等。

  • 注:JDK1.8 使用元空间 MetaSpace 替代方法区,元空间并不在 JVM中,而是使用本地内存。元空间两个参数:
    MetaSpaceSize:初始化元空间大小,控制发生GC阈值
    MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存

二、GC算法

JVM内存模型与GC机制

2.1 确定垃圾算法

2.1.1 引用计数法

在 Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单 的办法是通过引用计数来判断一个对象是否可以回收。简单说,给每个对象一个引用计数器,每当有一个地方引用它时,计数器就会加1;当引用失效时,计数器的值就会减1;任何时刻计数器的值为0的对象就是不可能再被使用的。

存在循环引用问题。

2.1.2 可达性分析

通过一系列称为“GC Roots”的对象为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是GC Roots到这个对象不可达)时,则说明此对象是不可用的。

要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

  • 那么那些对象可以作为GC Roots呢?以Java为例,有以下几种:

    1、栈(栈帧中的本地变量表)中引用的对象。

    2、方法区中的静态成员。

    3、方法区中的常量引用的对象(全局变量)。

    4、本地方法栈中JNI(一般说的Native方法)引用的对象。

    注:第一和第四种都是指的方法的本地变量表,第二种表达的意思比较清晰,第三种主要指的是声明为final的常量值。

2.2 垃圾收集算法

2.2.1 标记清除算法(mark-sweep)

最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清 除阶段回收被标记的对象所占用的空间
缺陷:大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题

JVM内存模型与GC机制

2.2.2 复制算法(coping)

为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小 的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉。
缺陷:这种算法虽然实现简单,内存效率高,不易产生碎片,但是大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,Copying算法的效率会大大降低

JVM内存模型与GC机制

2.2.3 标记整理算法(mark-compact)

结合了以上两个算法,为了避免缺陷而提出。标记阶段和Mark-Sweep算法相同,标记后不是清理对象,而是将存活对象移向内存的一端,然后清除端边界外的对象。

JVM内存模型与GC机制

2.2.4 分代收集算法

分代收集法是目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存 划分为不同的域,一般情况下将GC堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收新生代的特点是每次垃 圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法

1.新生代

是用来存放新生的对象。一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发 MinorGC进行垃圾回收。新生代又分为 Eden区、ServivorFrom、ServivorTo三个区。

JVM内存模型与GC机制

JVM内存模型与GC机制

新生代-复制算法:目前大部分JVM的GC 对于新生代都采取复制算法,因为新生代中每次垃圾回收都要 回收大部分对象,即要复制的操作比较少,但通常并不是按照1:1来划分新生代。一般将新生代 划分为一块较大的Eden空间和两个较小的Survivor空间(From Space, To Space),每次使用 Eden空间和其中的Survivor From空间,当进行回收时,将该两块空间中还存活的对象复制到Survivor To空间中

MinorGC的过程
采用复制算法
JVM内存模型与GC机制

当对象在Survivor区躲过一次GC 后,其年龄就会+1。默认情况下年龄到达15 的对象会被 移到老生代中

2.老年代

老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行 了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足 够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。

老年代-标记整理算法:因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用“标记—清理”或“标 记—整理”算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存。

MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没 有标记的对象。MajorGC的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减 少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的 时候,就会抛出OOM(Out of Memory)异常。

2.2.5 分区收集算法

分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的 好处是可以控制一次回收多少个小区间 , 根据目标停顿时间, 每次合理地回收若干个小区间(而不是 整个堆), 从而减少一次GC 所产生的停顿。

三、垃圾回收器

Java 堆内存被划分为新生代和年老代两部分,新生代主要使用复制和标记-清除垃圾回收算法; 年老代主要使用标记-整理垃圾回收算法,因此 java 虚拟中针对新生代和年老代分别提供了多种不 同的垃圾收集器,JDK1.6中Sun HotSpot虚拟机的垃圾收集器如下

JVM内存模型与GC机制

3.1 新生代垃圾收集器

3.1.1 Serial

基本垃圾收集器,使用复制算法。Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工 作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。 Serial 垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限 定单个 CPU 环境来说,没有线程交互的开销,可以获得高的单线程垃圾收集效率,因此 Serial 垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器

3.1.2 ParNew

ParNew垃圾收集器其实是Serial收集器的多线程版本,也使用复制算法,除了使用多线程进行垃 圾收集之外,其余的行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也 要暂停所有其他的工作线程
ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过-XX:ParallelGCThreads 参数来限 制垃圾收集器的线程数。【Parallel:平行的】 ParNew虽然是除了多线程外和Serial收集器几乎完全一样,但是ParNew垃圾收集器是很多java 虚拟机运行在Server模式下新生代的默认垃圾收集器

3.1.3 Parallel Scavenge

Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃 圾收集器,它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU 用于运行用户代码 的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)), 高吞吐量可以高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而 不需要太多交互的任务。

自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个 重要区别

GC自适应调节策略:Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。

Parallel Scavenge收集器使用两个参数控制吞吐量:

  • XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
  • XX:GCRatio 直接设置吞吐量的大小。

3.2 老年代垃圾收集器

3.2.1 Serial Old

Serial Old 是 Serial 垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法, 这个收集器也主要是运行在Client默认的java虚拟机默认的年老代垃圾收集器
Server模式下,主要有两个用途:
(1)在JDK1.5之前版本中与新生代的Parallel Scavenge收集器搭配使用。
(2)作为年老代中使用CMS收集器的后备垃圾收集方案。 新生代Serial与年老代Serial Old搭配垃圾收集过程图:

JVM内存模型与GC机制

(3)新生代Parallel Scavenge收集器与ParNew收集器工作原理类似,都是多线程的收集器,都使 用的是复制算法,在垃圾收集过程中都需要暂停所有的工作线程。新生代Parallel Scavenge/ParNew与年老代Serial Old搭配垃圾收集过程图:

JVM内存模型与GC机制

3.2.2 Parallel Old

Parallel Old收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在JDK1.6 才开始提供。 在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只 能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在年老代同样提供吞 吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge 和年老代Parallel Old收集器的搭配策略。 新生代Parallel Scavenge和年老代Parallel Old收集器搭配运行过程图:

JVM内存模型与GC机制

3.2.3 (CMS)

Concurrent mark sweep收集器是一种年老代垃圾收集器,其主要目标是获取短垃圾 回收停顿时间和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。 短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。 CMS工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下4个阶段:
初始标记只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程
并发标记:进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程
重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记 记录,仍然需要暂停所有的工作线程。
并发清除:清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时长的并 发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看 CMS收集器的内存回收和用户线程是一起并发地执行。

JVM内存模型与GC机制

3.2.4 G1收集器

G1收集器:Garbage first 垃圾收集器是目前垃圾收集器理论发展的前沿成果,相比与CMS 收集器,G1 收 集器两个突出的改进是:
1.基于标记-整理算法,不产生内存碎片。
2.可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。 G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域 的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾 多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得高的垃圾收 集效率