Java教程

5. JVM垃圾回收

本文主要是介绍5. JVM垃圾回收,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

文章目录

  • 1 标记阶段算法
    • 1.1 引用计数算法
    • 1.2 可达性分析算法
  • 2 清除阶段算法
    • 2.1 标记-清除算法
    • 2.2 复制算法
    • 2.3 标记-压缩算法
    • 2.4 分代收集算法
    • 2.5 增量收集算法
    • 2.6 分区算法
  • 3. 垃圾回收器
    • 3.1 垃圾回收器分类
    • 3.2 评估GC的性能指标
    • 3.3 垃圾回收器之间关系
    • 3.4 Serial垃圾回收器——串行回收
    • 3.5 ParNew垃圾回收器——并行回收
    • 3.6 Parallel 垃圾回收器——吞吐量优先
    • 3.7 CMS垃圾回收器——低延迟
    • 3.8 G1垃圾回收器——区域分代化
    • 3.9 其他垃圾回收器

1 标记阶段算法

标记阶段算法指,通过算法标记出内存中垃圾对象,以便后续清除阶段清除垃圾对象。
什么是垃圾对象呢?垃圾对象(Garbage Object)指程序运行中没有任何指针指向的对象,这种对象就是要被回收的对象。如果不及时对这些垃圾对象回收,这些对象占据内存空间,容易导致内存溢出。
对于内存中的对象,首先在标记阶段被标记为了垃圾对象,在清除阶段才会被清除。区分内存中哪些是存活对象,哪些是垃圾对象的阶段即为垃圾标记阶段。垃圾标记阶段判断兑现是否存活有两种方式:引用计数算法可达性分析算法

1.1 引用计数算法

引用计数算法(Reference Counting)对每个对象保持一个整型的引用计数器属性,用于记录对象被引用情况。比如对于一个对象A,只要有栈上的变量指向了对象A,A的引用计数器就加1,A如果被n个变量引用了,A的计数器就加n;当栈上的变量引用A对象失效时,比如局部方法执行结束后,对应的栈帧出栈,A对象的引用计数器减1,当所有的变量引用对象A都失效后,A的计数器值为0,表示A不再被引用,可以进行回收了。

引用计数器的优点:
实现简单,垃圾对象便于定位;判定是否垃圾对象效率高,回收没有延迟性。

引用计数器缺点:

  • 需要单独的字段存储计数器,增加了空间开销;
  • 每次都需要更新计数器,伴随着加减法操作,增加了时间开销;
  • 引用计数器非常严重的一个弊端,无法处理循环引用的对象,容易导致内存泄漏,这一致命缺点导致java的垃圾回收器未使用该算法。

例如对于如下示例,p对象引用了a对象,a的引用计数器为1,a引用了b,b的引用计数器为1,b引用了c,c的计数器值为1,c又引用了a,a的引用计数器为2。当p对象不再使用时,比如领p=null,此时a、b、c形成了一个循环引用的闭环,导致三者任何一个对象的计数器都不为0,无法进行回收,导致内存泄漏。
在这里插入图片描述

1.2 可达性分析算法

可达性分析算法又叫根搜索算法或者追踪性垃圾收集算法。可达性分析算法不仅具有实现简单、执行效率高,更重要的是它可以解决引用计数算法中循环引用的问题,防止内存泄漏。可达性分析算法就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可达的,不可达的对象就可以在GC时被回收了。在可达性分析算法中,只有被根对象集合直接或间接连接的对象才是存活对象。
例如,对于如下对象,从GC Roots触发,无法到达Object5、Object6、Object7,因此这三个对象会被标记垃圾对象,GC时会被回收。
在这里插入图片描述
哪些对象可以作为GC ROOT对象呢?

  1. 虚拟机栈中引用的对象,就是存储在局部变量表中的对象的变量。比如各个线程被调用的方法中使用到的参数、局部变量等;
  2. 本地方法栈内JNI(通常说本地方法)引用的对象;
  3. 类的静态属性引用的变量;
  4. 方法区中常量引用的对象,比如字符串常量池里的引用;
  5. 所有被同步锁synchronized持有的对象;
  6. java虚拟机内部的引用。比如基本数据类型对应的classes对象,一些常驻的异常对象(NullPointerExeption、OutOfMemoryError),以及系统类加载器
  7. 反映java虚拟机内部情况的MVXBean、JVMTI中注册的回调、本地代码缓存等。
    **以上7中方式都遵循一个规则:**由于Root对象采用栈方式存放变量,如果一个对象的变量它指向了堆内存中的对象,但是对象变量本身没有存储在堆中,那么这个堆内存中的这个对象就是一个Root对象。

除了上述7中固定的GC Root集合外,根据用户选择的垃圾收集器以及当前回收的内存区域不同,还会有其它对象被标记为GC Root,比如分代收集和堆内存局部回收(Partial GC)。对于这种特殊案例,例如只对java堆中某一内存区域进行垃圾回收(比如典型的只对新生代进行回收),这个区域的对象有可能被其它区域的对象引用,比如有可能老年代一个对象在引用新生代中一个对象,这是就要把新生代关联的其他区域中的对象也加入GC Root集合中,此地就是把关联的老年代对象加入到GC Root集合中,这样才能保证可达性分析的准确性。

注意:如果用可达性分析来确认垃圾对象,必须保证分析工作在一个能保障一致性的快照中进行,否则就不能准确分析垃圾对象。因为如果不在一致性的快照中分析,加入当前正在分析一个GC Root对象,下一秒可能该对象就属于GC Root对象了。因此正是由于此种原因,GC时必须进行Stop The World,暂停所有线程的执行,内存中对象不会改动,此时用一致性算法分析是否垃圾对象。即使号称几乎不会发生停顿的CMS收集器,枚举根节点时也是必须要停顿的。

2 清除阶段算法

在标记阶段通过标记算法区分出垃圾对象后,接下来清除阶段就是通过清除算法回收垃圾对象占用的空间。目前常见的清除算法包括:标记-清除算法(Mark-Sweep),复制算法(Copying)、标记-压缩算法(Mark-Compack)。

2.1 标记-清除算法

当堆中的有效内存空间(available memory)被耗尽的时候,会暂停整个程序(Stop the world),然后进行两项工作:标记和清除。
标记:从引用的根节点遍历,标记所有被引用的对象,一般是在被引用对象的Header中记录为可达对象;
清除:对堆中所有的对象进行遍历,如果发现某个对象的Header中没被标记为可达对象,则回收该对象。

如下图所示,绿色的为可达对象,黑色的为非可达对象,则黑色的区域内存就会被回收,堆中空闲内存增加,供后续对象内存分配。其实回收黑色的区域内存并不是把该区域内存中的内容进行清空,而是标记该区域为空闲区域,尽管该区域中内容还在,但下次分配对象时就可以直接在这部分标记空闲的区域内存中进行分配,覆盖掉原来的内容,节省了清除黑色区域内存中内容的时间。
标记黑色区域为空闲区域,就是把需要清除对象的黑色区域的地址保持在空闲列表中,下次有新对象分配时,判断空闲列表中记录的内存区域是否足够存放新分配的对象。
在这里插入图片描述
标记清除算法的缺点

  • 效率不高;

  • GC的时候需要暂停整个应用程序,用户体验差;

  • 这种方式清理出的空闲内存是不连续的,产生内存碎片,需要维护一个空闲列表,空闲列表就是记录堆内存中空闲的区域,也即可达对象占用内存以外的内存空间。

2.2 复制算法

复制算法就是把堆空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的一块内存中可达对象复制到未使用的另一块内存中,然后清除正在使用这一块内存中所有对象,交换两块内存的角色,完成垃圾回收。下次垃圾回收时,重新交换两块内存的角色。 就像新生代中的Survinor0和Survinor1一样。

如下图所示,垃圾回收时把A中可达对象复制到B中。
在这里插入图片描述
复制算法优点

  1. 没有标记和清除过程,实现简单,运行高效;
  2. 复制算法解决了空间碎片化问题。

复制算法缺点

  1. 需要2倍的空间,空间浪费;
  2. 对象移动导致地址该表,栈上变量引用对象的地址也需要改变。

如果系统中垃圾对象很多,那么垃圾回收时,复制算法需要移动复制的对象比较少,是比较适合用复制算法的。

2.3 标记-压缩算法

复制算法适合垃圾对象多,存活对象少的场景,比如新生代,但是在老年代,这种存活对象多的场景,复制算法成本会很高。而标记-清除算法用于老年代,该算法不仅执行效率低下,而且会产生内存碎片,基于此标记-压缩(Mark-Compact)算法应运而生。
标记-压缩算法第一阶段与标记-清除算法一样,从根节点开始标记有效存活的对象,第二阶段将所有存活的对象压缩到内存一端,按顺序排放,之后清理边界外的所有垃圾对象。如下图所示
在这里插入图片描述
标记-清除算法只清除垃圾对象,不移动存活对象,而标记-压缩算法标记对象,还要移动规整存活对象。如此一来,当需要新分配对象时,对于标记-压缩算法,JVM只需要维护一个对象的内存起始地址就可定位到该对象;而对于标记-清除算法,JVM需要维护一个空闲列表,标记该对象都占据了哪些内存,显然维护空闲列表开销更大。
对于标记-清除算法,分配对象时采用空闲列表分配法,对于标记-压缩算法,分配对象时采用指针碰撞方法,具体参考java对象实例化内存布局与访问定位章节中的内存分配。

标记-压缩算法优点

  1. 解决了标记-清除算法中内存碎片化问题;
  2. 解决了复制算法中,内存减半的高额代价。

标记-压缩算法缺点

  1. 效率上,标记-压缩算法低于复制算法;
  2. 压缩对象时,如果该对象被其它对象引用,需要调整引用地址;
  3. 移动对象过程中,需要暂停用户线程,即STW。

标记-清除算法、复制算法、标记-压缩算法对比如下:
在这里插入图片描述

2.4 分代收集算法

前面提到的3中算法各自有不同的优缺点,适应场景不同,互相不能替代,因此分代收集算法应运而生。
分代收集算法就是:不同对象的声明周期不同,不同生命周期的对象采取不同的收集算法,以便提高回收效率。一般java堆分成新生代和老年代,不同的代之间采用不同的收集算法,可以提高垃圾回收效率。目前几乎所有的GC都采用分代收集算法(Generational Collecting)来进行垃圾回收。

下面分析下新生代和老年代各采用什么算法进行回收垃圾:

  1. 年轻代(Young Gen)
    年轻代的特点就是对象生命周期短、存活率低,回收频繁,内存区域相对老年代比较小,这种情况下比较适合复制算法,复制算法速度最快,复制算法的效率只与新生代中存活对象的大小有关,存活的对象越少,越适合复制算法。而复制算法空间利用率不高的问题,通过hotspot虚拟机中的两个survivor的设计得到缓解,因此年轻代比较适合复制算法。

  2. 老年代(Tenured Gen)
    老年代内存区域比较大,其中的对象生命周期长、存活率高,回收没有年轻代频繁。这种情况不适合复制算法,一般是由标记-清除算法与标记-压缩算法混合实现。影响标记-清除算法与标记-压缩算法空间效率如下:

    Mark标记阶段的空间开销与存活对象的数量成正比;
    Sweep清除阶段的空间开销与所管理区域的大小成正比;
    Compact压缩阶段的空间开销与存活对象的大小成正比。

分代收集思想被现有的虚机广泛使用,机会所有的垃圾回收器都区分新生代和老年代。

2.5 增量收集算法

在上面几种算法中,垃圾回收过程中,虚拟机都会暂停用户线程Stop The World,一直到垃圾回收完毕,如果垃圾回收的时间很长,严重影响用户的体验以及系统的稳定性。为了解决这个问题,增量收集算法(Incremental Collecting)算法应运而生。
一次性进行垃圾回收,造成系统停顿时间长,通过增量收集算法,每次垃圾线程只收集一小片区域的内存空间,接着切换到应用线程执行应用程序。依次反复,直到垃圾收集完成。
总之增量垃圾回收算法的基础仍然是传统的标记-清除和复制算法,增量收集算法通过对线程间的冲突妥善处理,允许垃圾手机线程以分阶段的方式完成标记-清理或者复制工作。

增量收集算法除了可有降低一次性垃圾回收时间过长问题,但是这种方式在垃圾回收过程中,间断性性的回收垃圾和间断性的执行应用程序代码,虽然减少了系统一次性停顿的时间,但是垃圾线程和用户线程之间的频繁切换,使得垃圾回收的总成本上升,造成系统吞吐量的下降。

2.6 分区算法

分区算法与增量回收算法一样,都是为了避免一次GC消耗时间过长而产生的。为了能更好的控制GC停顿的时间,将一块大的内存区域分割成若干小块,根据设定的回收时间,每次合理地回收若干小区间,而不是整个堆空间,从而减少一次GC消耗的时间。
分区算法中每一个小区间都独立使用,独立回收,这种算法好处就是可以控制一次回收多少个小区间。如下图所示,堆空间可以划分成多个Eden、Survivor、Old区,其中白色为可用区域,比如只想GC时暂停100ms,JVM在规定的时间内只回收了10个小区间,下次GC时可以进行回收剩下的小区间。通过这种方式可以设定GC回收时间,降低GC在整个堆中一次性回收时间过长消耗。
在这里插入图片描述
分代算法是按照对象生命周期长短划分成两个部分,而分区算法就是把这两个部分继续划分成多个region小空间。
增量算法与分区算法区别:增量算法是Eden、Survivor0/Survivor1、Old中各一部分内存空间,比如对Eden进行垃圾回收时,只回收Eden中以小部分,下次GC再回收一小部分,依次往复直到全部回收完毕。而分区算法是Eden、Survivor0/Survivor1、Old在堆内存中有很多部分,即有很多个Eden、Survivor0/Survivor1、Old区,每个区都是独立的一个部分,每次垃圾回收时回收几个独立的小部分即可,比如堆内存中有很多个Eden区,垃圾回收时可以规定100ms时间内尽可能去收集Eden中region小空间,比如100ms内只能收集10个Eden的小空间。增量算法中Eden是一个整体的堆空间,分区算法中有多个Eden小空间。

3. 垃圾回收器

3.1 垃圾回收器分类

垃圾回收器没有在规范中过多规定,可以由不同厂商、不同版本的JVM来实现。从不同的角度分析,垃圾回收器可以分为不同的类型。
1. 按照垃圾回收线程的线程数量划分,可以分为串行垃圾回收器和并行垃圾回收器
串行垃圾回收器指在同一时间段内,只允许由一个处理器执行垃圾回收操作,此时用户线程被stop the world,直到垃圾回收器结束。
并行垃圾回收器指在同一时刻可以由多个处理器同时并行执行垃圾回收工作。
在这里插入图片描述

串行垃圾回收器主要用在硬件配置低,比如内存配置比较小、处理器较少的场合,此时串行垃圾回收器比并行垃圾回收器有优势。比如只有一个处理器例子,如果用串行垃圾回收器,等垃圾线程执行完毕,然后在执行用户线程,而如果换到并行垃圾回收器,在只有一个处理器场景下,多个垃圾回收线程要不停的切换,此时效率不仅没有提高,反而消耗了垃圾线程之间切换的时间,效率更为低下。因此JVM在客户端的client模式下默认的垃圾处理器就为串行垃圾回收器。
并行垃圾回收器适合在高配置场景下,多处理器场合。此时垃圾线程并行执行,可以减少垃圾回收时间,提高应用吞吐量。不过并行垃圾回收与串行回收一样,都是采用的独占式机制,垃圾回收时要暂停用户线程,stop the world。

2. 按照垃圾回收的工作模式,可以分为并发时垃圾回收器和独占式垃圾回收器
并发时垃圾回收器垃圾回收线程可以与应用线程同时执行(不一定是并行,也可能是交替执行),较少用户程序等待时间。并发与并行垃圾回收器区别,参考文章
独占式垃圾回收器工作式就会暂停所有用户线程执行,直到垃圾回收工作结束,用户等待时间可能较长。
在这里插入图片描述

3. 按照垃圾回收工作的碎片处理方式,可以分为压缩式垃圾回收器和非压缩式垃圾回收器
压缩式垃圾回收器指在回收完垃圾对象后,对堆内存空间中存活的对象进行压缩整理。
非压缩式垃圾回收器在回收完垃圾对象后,不会进行堆空间存活对象整理。

4. 按照垃圾回收的工作空间,又可以分为年轻代垃圾回收器和老年代垃圾回收器
专门用来回收年轻代区间对象的为年轻代垃圾回收器。
专门用来回收老年代区间对象的为老年代垃圾回收器。

3.2 评估GC的性能指标

吞吐量:指用户线程执行时间占总执行时间的比例。总执行时间=用户线程执行时间+垃圾回收线程执行时间。
垃圾收集开销:吞吐量的补数,指垃圾回收线程执行时间占总执行时间的比例。
暂停时间:执行垃圾回收工作时,用户线程被暂停的时间。
收集频率:垃圾回收工作发生的频率,即GC的频率。
堆内存空间大小:指堆空间占整个内存空间的大小。如果堆空间太小,发生GC的频率太高,因此尽量堆空间调大。
回收速度:一个对象从诞生到被回收所经历的时间。

以上性能指标中,其中吞吐量、暂停时间、堆内存空间大小是最为关注的,任何垃圾回收器都不可能同时满足三者,一款优秀的垃圾回收器最多同时满足其中两者。比如堆内存增大时,程序的吞吐量也会增加,但是暂停的时间会变长,因为内存增加,回收的频率减少了,一次回收需要的时间也相应边长;如果要减少用户线程暂停时间,那么垃圾回收的频率也应相应增加,相反吞吐量变差。


吞吐量、暂停时间、堆内存空间三个指标中,由于随着配置越来越高,堆内存空间问题越来越容易满足,因此一款优秀的垃圾回收器尤其要关注吞吐量和用户暂停时间,但也只能满足两者其一,或增大吞吐量,或减少用户线程暂停时间,或者尝试找到二者的折衷。但高吞吐量和低暂停时间(也叫低延迟)是一对矛盾目标:

  • 如果选择以高吞吐量优先,就必须要降低内存回收的频率,但是就会导致一次性GC需要更长的暂停时间来回收内存;
  • 相反,如果以低延迟为优先原则,那么为了降低每次垃圾回收暂停的时间,也只能频繁的执行垃圾回收,但又会导致程序的吞吐量下降。

因此当前垃圾回收器追寻的目标:在最大吞吐量优先的情况下,尽可能减小用户线程暂停的时间。

3.3 垃圾回收器之间关系

在JDK9之前主要有以下7款经典垃圾回收器:

  • 串行垃圾回收器:Serial、Serial Old
  • 并行垃圾回收器:ParNew、Parallel Scavenge、Parallel Old
  • 并发垃圾回收器:CMS、G1

7种垃圾回收器种,其中Serial、Parallel Scavenge、ParNew用于回收新生代垃圾对象;Serial Old、Parallel Old、CMS用于老年代垃圾对象回收;G1用于整堆垃圾回收,既可以回收新生代垃圾对象,又可以回收老年代垃圾对象。如下图所示
在这里插入图片描述

7中垃圾回收器的组合关系如下,在JDK8之前,新生代可以 使用SerialGC时,老年代可以使用CMS GC或者Serial Old GC;新生代使用ParNew GC时,老年代可以使用CMS GC或者Serial Old GC;新生代使用Parallel Scavenge时,老年代可以使用Parallel Old GC或者Serial Old GC。从图中可以看出CMS GC和Serial Old GC也有关系,是CMS GC如果失败的情况下(比如垃圾制造的速度大于对象回收的速度),Serial Old GC可以看做是CMS GC的一个后备方案。
在JDK8时,把SerialGC+CMS组合以及ParNew GC+Serial Old GC的组合标记为废弃,在JDK9时取消了这两对组合垃圾回收器的配合使用。
在JDK14时,把Parallel Scavenge GC + Serial Old GC组合标记为废弃,并且删除了CMS垃圾回收器。至此7种经典的垃圾回收器只剩Serial GC+ Serial Old GC、Parallel Scavenge GC + Parallel Old GC以及整堆收集的G1 GC。
在这里插入图片描述

3.4 Serial垃圾回收器——串行回收

Serial垃圾回收器作为HotSpot虚拟机中Client模式下默认的新生代垃圾回收器,是垃圾回收器中最基本、最悠久的一款垃圾回收器了,JDK1.3之前版本唯一选择的垃圾回收器。
Serial垃圾回收器采用串行回收内存方式,回收内存时Stop the world,回收内存时采用的前文结束的复制算法。
Serial与Serial Old可以配合使用,Serial用于回收新生代内存,Serial Old用于回收老年代内存。Serial Old与Serial原理一致,底层框架用的同一套,因此Serial Old也是串行回收,并且回收老年代时Stop the world,但是Serial Old回收垃圾对象时采用的是前文介绍的标记-压缩算法,当然该算法速度也是最慢的,导致了Serial Old回收的效率也不高。

Serial Old垃圾回收器是Hotspot虚拟机Client模式下默认的老年代垃圾回收器;
Serial Old垃圾回收器在Server模式下主要有两种用途:与新生代的Parallel Scavenge配合使用;作为老年代CMS垃圾回收器的后背垃圾收集方案。

在这里插入图片描述

Serial回收器的优势:对于单个CPU处理器来说,Serial/Serial Old垃圾回收器简单高效,因为单个垃圾回收线程在回收垃圾对象时,没有线程之间的交互,专心做垃圾清理工作,工作效率高。因此单处理器、client场景下,Serial/Serial Old是不错的选择。

Serial/Serial Old的使用:使用过程中,可以通过指定 -XX:+UseSerialGC 参数来标识堆空间新生代使用Serial垃圾回收器,老年代使用Serial Old垃圾回收器。可以通过使用jinfo -flag UseSerialGC 进程号 来查看是否使用的Serial垃圾回收器。

3.5 ParNew垃圾回收器——并行回收

ParNew是Parallel New的缩写,new代表新生代,因此ParNew也只能处理新生代内存回收。ParNew是Serial的多线程版本。ParNew除了采用并行方式回收垃圾对象外,与Serial垃圾回收器几乎没什么区别了,ParNew回收年轻代对象时采用的复制算法,在执行过程中,同样需要Stop the world。
在这里插入图片描述

如上图所示,对于新生代,回收次数频繁,使用并行方式效率更高;对于老年代,回收次数少,使用串行方式Serial Old更节省资源(因为CPU并行需要切换线程,串行可以省去切换线程的资源)。ParNew除了可以与Serial Old搭配使用外,ParNew还可以与CMS搭配使用。

ParNew虽然是并行的,但也不意味着ParNew在任何情况下都比Serial垃圾回收器的效率高:

  • 在多CPU处理器场景下,ParNew并行可以充分利用多CPU、多核心等物理硬件资源,可以更快速的完成垃圾收集工作,提升程序的吞吐量;
  • 然而在单CPU处理器场景下,虽然ParNew是基于并行的,Serial是基于串行的,单Serial的效率不比ParNew的效率低,因为Serial有效的避免了多线程交互中产生的额外开销。

ParNew的使用:在启动JVM之前,可以手动设置-XX:UseParNewGCJVM参数,该参数表示年轻代使用ParNew垃圾回收器,不会影响老年代垃圾回收器使用情况,老年代需要单独另配置。在设置-XX:UseParNewGC参数时,也可以同时设置-XX:ParallelGCThreads,该参数用于设置垃圾并行线程数量,如不设置线程数量默认与CPU数据相同。

3.6 Parallel 垃圾回收器——吞吐量优先

HotSpot的年轻代中除了ParNew是基于并行回收外,Parallel Scavenge也同样是基于并行,并且Parallel Scavenge同样也采用了复制算法回收垃圾对象,在执行过程中也会Stop the world。Parallel Scavenge垃圾回收器是以吞吐量优先的垃圾回收器,而高吞吐量就要求要高效的利用CPU,尽快完成用户程序的执行任务,因此Parallel Scavenge适合后台运算而不需要太多与用户交互的任务,比如批量处理、大量运算等工作。为什么呢?前面介绍过,对于高吞吐量的垃圾回收器,那么他的回收频率就会低,一次性回收垃圾对象的时间也会边长,延迟高,对于用户打交道的程序,会让客户感觉延迟时间较长,体验不好,因此对于高吞吐量,更适合后台执行的程序,与用户打交道少。
Parallel Scavenge比ParNew的优势如下

  • 和ParNew不同的是,Parallel Scavenge垃圾回收器的目标是达到一个可控制的吞吐量(Throughput),因此Parallel Scavenge也被称为吞吐量优先的垃圾回收器;
  • Parallel Scavenge的另一个重要用途是自适应策略,内存可以自适应调整。

在JDK6之前,与Parallel Scavenge搭配使用的老年代垃圾回收器是Serial Old,比较鸡肋的是,年轻代用的并行垃圾回收,老年代却用的是串行回收。因此呢在JDK6时又提供了Parallel Old垃圾回收器,专门用来回收老年代内存,用来代替Serial Old,与Parallel Scavenge搭配使用。Parallel Old垃圾回收器采用的是标记-压缩算法用来回收垃圾对象,采用并行进行回收,在执行时也需要Stop the world。如下图所示
在这里插入图片描述

在吞吐量优先的场景中,Parallel Scavenge和Parallel Old配合使用,在JVM的server模式下,内存回收性能不错,因此在JDK8中,默认的垃圾回收器为Parallel。


Parallel垃圾回收器参数设置

  • -XX:+UseParallelGC 虚拟机启动时设置该参数,可以手动设置堆新生代采用Parallel垃圾回收器回收垃圾对象
  • -XX:+UseParallelOldGC 虚拟机启动时设置该参数,可以手动设置老年代采用Parallel Old垃圾回收器回收垃圾对象。-XX:+UseParallelGC-XX:+UseParallelOldGC参数两者设置其中任何一个,另一个都会自动激活生效。当然如果在JDK8中,两者可以都不必设置,JDK8中默认就是采用的Parallel GC + Parallel Old GC。
  • -XX:ParallelGCThreads 表示回收年轻代垃圾回收线程的个数,一般垃圾回收线程的个数与CPU处理器的个数相等,如果垃圾回收线程数量多于处理器的个数,那么就会出现在同一个处理器中涉及多个线程相互切换,交替执行,增大了资源消耗。另外当处理器的个数多于8个时,ParallelGCThreads 的计算公式可以为 3+[5*CPU_COUNT/8]。
  • -XX:MaxGCPauseMills 设置垃圾回收器最大暂停时间(单位为ms),即STW的时间。为了尽可能的控制垃圾回收器STW的时间在MaxGCPauseMills设置的以内,垃圾回收器会自适应的调整java堆大小或者其他一些参数。对于用户来说,暂停时间越短,体验感越好,对于与用户交互的程序来说,既然要缩短STW的时间,那么就要高并发处理垃圾线程,提升程序的整体吞吐量。
  • -XX:GCTimeRatio 表示垃圾收集时间占总程序执行的执行时间(包括垃圾处理的时间),即1/(n+1),其中-XX:GCTimeRatio=n,用于衡量吞吐量的大小。该参数取值范围为0~100。默认值为99,即垃圾回收时间占比程序总执行时间不能超过1%。 -XX:MaxGCPauseMills参数与-XX:GCTimeRatio参数是一对矛盾体,如果设置 -XX:MaxGCPauseMills的值小,程序每次暂停的时间短,那么相应的垃圾回收的频率就会增加,相应的垃圾回收的总时间就会边长,因为回收频率高,就意味着用户线程与垃圾线程之间切换的频率就会比较高,相比用户线程与垃圾线程切换频率小的耗时长(设置暂停时间短与设置暂停时间长,垃圾线程本身执行的总时间是一致的,只是设置时间段的,垃圾线程与用户线程之间切换频率高,因此设置暂停时间短比设置暂停时间长导致垃圾回收时间长的一部分时间正是线程之间切换消耗的部分时间),因此-XX:MaxGCPauseMills设置的小,那么垃圾收集总时间就会高,即n的也要变小;反之-XX:MaxGCPauseMills设置过大,垃圾回收的总时间也会变少。
  • -XX:+useAdaptiveSizePolicy 设置Parallel Scavenge垃圾回收器具有自适应特性。开启自适应特性后,年轻代的大小、Eden和Survivor的比例、晋升到老年代的对象年龄等参数都会自动调整,以达到堆大小、吞吐量和停顿时间之间的平衡点。在手动调优困难场合下,可以直接设置开启自适应特特性后,仅指定虚拟机最大堆、目标吞吐量(GCTimeRatio)和用户暂停时间(MaxGCPauseMills),让虚拟机自动完成调优工作。

3.7 CMS垃圾回收器——低延迟

在JDK1.5中,Hotspot虚拟机推出了一款在强交互应用中有划时代意义的垃圾收集器CMS(Concurrent-Mark-Sweep),CMS是第一款真正意义上的并发回收器,第一次实现了让垃圾线程与用户线程同时工作。
CMS垃圾回收器关注点是尽可能的缩短垃圾回收时用户线程暂停的时间,停顿时间越短(低延迟)就越适合与用户程序交互,良好的响应速度能提升用户的体验。目前很大一部分java应用是部署在互联网网站或者B/S系统的服务器上,这类服务尤其重视服务的响应速度,希望降低系统的停顿时间,以便给用户带来良好的体验,CMS垃圾回收器恰好满足此类需求。
CMS采用的是标记-清除算法,在执行过程中也会Stop the world。CMS只能作为老年代的垃圾回收,需要配合新生代ParNew或者Serial垃圾回收器使用,但不能配合新生代中的Parallel Scavenge垃圾回收器,因为二者底层用的框架不一致。
在这里插入图片描述

CMS垃圾回收整个过程分为部分:初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段。

  • 初始标记阶段(Initial-Mark):在初始标记阶段,会短暂出现Stop the world,用户线程被短暂的暂停,这个阶段仅仅只是标出GC Roots能直接关联的对象。一旦标记完成后就会恢复被暂停的用户线程。由于只是标记对象,所以被暂停的时间会比较短。
  • 并发标记阶段(Concurrent-Mark):从GC Roots直接关联的对象开始遍历整个对象图的过程,虽然这个过程耗时长,但不需要暂停用户线程,用户线程可以与垃圾线程并发执行,垃圾线程会占用一部分处理器时间,降低系统的吞吐量。
  • 重新标记阶段(Remark):由于在并发阶段,垃圾收集线程和用户线程同时运行或交替运行,因此Remark阶段正是为了修正并发标记阶段,因用户线程继续运行而导致标记产生变动的那一部分对象的记录,这个阶段停顿时间会比初始阶段稍长一些,但也远比并发阶段的时间短。
  • 并发清除阶段(Concurrent-Sweep):此阶段清理标记阶段已经死亡的对象,释放内存空间。由于不需要移动存活对象,这个阶段也是可以用户线程并发执行的。

CMS垃圾回收器采用的Mark-Sweep标记清除算法,会造成内存碎片,为什么不采用Mark-Compact压缩整理算法呢?
采用Mark-Sweep并发清除垃圾对象时,不需要移动存活的对象,可以与用户线程并发执行,而如果采用Mark-Compact压缩整理算法,清除垃圾对象后,需要整理内存,就要移动存活对象,因此不能与用户线程并发执行。

CMS的优点

  • 并发收集:垃圾线程可以与用户线程并发执行;
  • 低延迟:由于最耗时的并发标记与并发清除阶段都不需要暂停工作,所以整体时间第低延迟的。

CMS的缺点

  • 产生内存碎片:由于CMS采用的标记-清除算法,清除垃圾对象后会产生内存碎片,那么CMS在为新对象分配内存时,无法使用指针碰撞(Bump the Pointer)技术,只能够选择空闲列表(Free List)技术进行内存分配,空闲列表技术还要额外分配一块内存用于标记空闲的内存。另外尽管空间充足,但是如果连续的空间中此时无法分配大对象情况下,就容易发生FULL GC;
  • 垃圾回收失败:由于垃圾回收阶段用户线程没有暂停,所以在CMS执行过程中,还应该确保用户线程有足够的内存空间可用,因为用户线程可能还会继续分配新对象。因此CMS不像其他回收器等老年代满了才开始回收垃圾对象,而是当堆内存使用率达到某一阈值时,便开始进行垃圾回收,确保用户线程在CMS执行过程中依然有足够空间支持对象分配。但是如果CMS运行期间,用户线程有大对象分配,预留的空间不足以分配新对象,此时就会出现“Concurrent Mode Failure”并发模式失败,这时就会启动预备方案,临时启用Serial Old垃圾回收器来重新进行老年代的垃圾回收,此时垃圾收集停顿的时间反而变长了;
  • 对CPU资源非常敏感:并发阶段,虽然不会导致用户线程暂停,但是因为垃圾线程占用了用户线程一部分资源,导致用户线程变慢,总吞吐量会降低;
  • 无法处理浮动垃圾:并发标记阶段用户线程与垃圾线程是同时运行或者交替运行的,那么并发阶段产生新的垃圾对象,CMS是无法进行标记清除的,最终导致新产生的对象没有被及时回收,只能等到下一次FULL GC。

CMS的参数设置

  • -XX:+UseConcMarkSweepGC 设定老年代使用CMS垃圾回收器。开启该参数后,会自动启动参数-XX:+UseParNewGC,即ParNew(新生代) + CMS(老年代) + Serial Old(老年代)组合使用;
  • -XX:CMSInitiatingOccupanyFraction 设置堆内存使用率的阈值,一旦达到该阈值,便开始进行垃圾回收。JDK5以及之前的版本的阈值默认为68,即老年代空间使用率达到68%时,会进行垃圾回收。而JDK6及以上版本的默认值为92%。如果内存增长缓慢,可以设置一个较大的阈值,可以有效降低触发CMS垃圾回收的频率,老年代回收的次数降低,可以提高系统的性能。反之如果内存增长较快,应该降低这个阈值,可以有效降低FULL GC的次数。
  • -XX:+UseCMSCompactAtFullCollection 指定FULL GC后对内存采取压缩整理,避免内存碎片产生。但是压缩整理无法并发执行,因为压缩整理涉及到移动对象,会影响正在执行的程序,因此如果需要压缩整理的话,用户线程停顿的时间就变得更长了。
  • -XX:CMSFullGCBeforeCompaction 设置执行多少次FULL GC后对内存进行压缩整理,与参数UseCMSCompactAtFullCollection配合使用。
  • -XX:ParallelCMSThreads 设置CMS线程的数量,CMS默认的线程数量为(ParallelGCThreads+3)/ 4,其中ParallelGCThreads是年轻代垃圾回收器的线程数。当CPU资源紧张时,受CMS垃圾回收线程影响,用户线程在并发执行时会受到影响。

CMS在JDK后续版本中的变化

  1. CMS在JDK9中被标记为Deprecate:当使用JDK9及以上版本时,使用参数-XX:+UseConcMarkSweepGC开启CMS垃圾回收器后,用户会受到警告,提示CMS未来会被废弃;
  2. CMS在JDK14中被删除:在JDK14中使用-XX:+UseConcMarkSweepGC参数开启CMS时,JVM不会报错,也不会exit,只会给一个warning信息,并且JVM会自动回退以默认的GC方式启动JVM。

3.8 G1垃圾回收器——区域分代化

随着现代不断扩大的内存和不断增加的处理器数量,G1官方给出的目标就是为了在延迟可控的情况下尽可能提高系统的吞吐量。

G1是一个并行垃圾回收器,它把内存分割为许多不相关的region区域,region物理上是不连续的,用不同的region来表示Eden、Surivivor、Old区。G1通过对不同的region进行内存回收,有效的避免了在整个java堆上进行垃圾回收。G1跟踪各个region里面垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要的时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的region。由于这种回收优先回收价值最大的region空间,因此G1又叫垃圾优先(Garbage
First)。

G1是一款面向服务器端的垃圾回收器,主要针对多核大容量的服务器。在JDK7版本正式启用,移除了Experimental标识,JDK9开始设置为默认的垃圾回收器,被oracle官方称为全功能的垃圾回收器。与此同时,JDK9中,CMS被标记为了废弃(deprecated)。在JDK8中如需使用G1,需要显示设置-XX:+UseG1GC。

3.8.1 与其他垃圾回收器比较,G1使用了全新的分区算法,其优势如下

  1. 并行与并发
    G1支持并行处理,在垃圾回收器期间可以多个GC线程同时执行,有效利用多核处理能力,此时用户线程STW。
    G1还支持并发处理,垃圾回收期间可以与用户线程交替执行,部分也可以与用户线程同时执行,一般情况下,不会有在整个回收阶段发生完全阻塞应用线程的情况。

  2. 分代收集
    G1仍属于分代垃圾收集,虽然G1仍保持了新生代和老年代的概念,但新生代和老年代不再固定了,他们都是一系列的region区域组成的动态集合,region物理上可以不连续。C1将堆空间划分若干region区域,用这些区域表示了逻辑上的年轻代和老年代。和之前打垃圾回收器不同,之前的垃圾回收器或者是工作老年代,或者是工作年轻代,同一时刻只能收集其中之一的内存,而G1可以同时兼顾新生代和老年代。

  3. 空间整合
    G1回收是以region为单位的,region之间采用复制算法,比如回收一个region时,可以这个将要回收region中存活的对象复制到另一个空间充足的region中,但从整个堆空间中看,又属于标记-压缩算法,因为有些region被回收后,G1会把剩下的region在堆空间中进行规整,避免了内存碎片问题。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续的内存空间而提前触发下一次GC,尤其是当堆空间比较大时,G1的优势更明显。

  4. 可预测的停顿时间模型
    由于G1跟踪各个region垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要的时间的经验值),在后台维护一个优先列表,每次根据允许垃圾收集的时间,优先回收价值最大的region,保证了G1垃圾回收器在指定的优先时间内尽可能的垃圾收集效率。比如使用者指定在程序执行的M秒时间内,希望垃圾收集占用的时间不超过N秒,G1就会根据计算出的region经验值,比如回收10个最优的region,垃圾回收时间不会超过N秒,通过这种方式,G1不仅可以在追求低延迟情况下,还可以建立可预测的时间模型,促使垃圾回收时间不要超过指定的时间。

  5. 垃圾回收线程
    其它垃圾回收器,都是垃圾回收线程回收垃圾对象,而G1可以使用用户线程进行垃圾回收,当JVM的GC线程处理速度缓慢时,系统会调用应用线程帮助垃圾线程尽快回收垃圾对象。

3.8.2 G1垃圾回收器的缺点
相比于CMS,G1还不具有全方位、压倒性的优势,比如在用户执行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint),还是程序运行过程中额外执行的负载(Overload)都要比CMS高。从经验看,小内存应用上的CMS表现优于G1的概率更大,而G1在大内存上更能发挥其优势,这个内存之间平衡点在6~8G之间。

3.8.3 G1垃圾回收器的参数设置

  1. -XX:+UseG1GC 手动指定使用G1垃圾回收器进行垃圾回收,JDK9之后可以不指定,默认使用;
  2. -XX:G1HeapRegionSize 设置每个region大小,其值为2的幂次方,region大小范围为1mb至32mb之间,目标是根据最小的java堆大小划分为2048个region区域,默认是堆内存的1/2000。但如果设置的region大小一定,堆内存足够大的情况下,region的个数也会更多;
  3. -XX:MaxGCPauseMills 设置期望达到的GC停顿的最大时间(JVM会尽力,但不保证),默认值是200ms;
  4. -XX:ParallelGCThread 设置并行垃圾线程数量,最大设置为8;
  5. -XX:ConcGCThreads 设置并发标记的线程数,可以将ConcGCThreads设置为ParallelGCThread 的1/4左右。
  6. -XX:InitiatingHeapOccupancyPercent 设置堆的占用率阈值,超过该阈值,触发GC,该值默认45。

3.8.4 G1垃圾回收器常见操作步骤
G1设计原则就是简化JVM调优,开发人员只需如下三步即可:

  1. 开启G1垃圾回收器;
  2. 设置堆的最大内存;
  3. 设置最带的停顿时间,让G1自动进行优化。
    G1提供了三种垃圾回收模式:YoungGC、MixedGC、FullGC,三种模式在不同的条件下触发。

3.8.5 G1垃圾回收器适用场景
G1适用面向服务端,针对大内存、多处理机器。最主要适用低延迟场景、并且有大堆的应用场景下,比如,堆为6g或者更大,预测暂停时间低于0.5s(G1通过每次只清理一部分region,而不是全部的堆,保证每次GC的时间不会过长)。

3.8.6 Region介绍
每个region根据堆空间的实际大小而定,整体被控制在1M到32M之间,且为2的N次幂,可以通过参数-XX:G1HeapRegionSize设定,所有的region大小相同,而且在JVM生命周期内不会被改变。
虽然还保留年轻代和老年代的概念,但新生代和老年代不再是物理隔离的了,他们都由不同的region组合,通过region的动态分配实现逻辑上的连续。
region图解如下所示,整个空间被划分为很多region,有尚未被使用的白色空间region区域,还有Eden/Surivivor/Old区的region,以及表示humongous的region。其中一个region在同一时刻只能属于一个角色,图中E表示region属于Eden区的region,S表示属于Survivor区的region,O表示属于Old区的region。另外,G1还增加了一种新的内存区域,叫做Humongous,如途中的H所示,主要用于存储大对象,如果对象占用内存超过1.5个region,就放到H区。
在这里插入图片描述
设置H区的原因:在G1垃圾回收器之前,对于超大对象是直接分配到老年代的,这种行为会提前触发老年代发生FullGC,尤其是对于只使用一次就发生GC的大对象,非常影响程序的执行效率。而在G1时划分了Humongous区,专门用来存放大对象,如果一个H区存不下大对象,就会找连续的H区存放大对象,如果找不到就会发生FullGCh回收内存,G1的大多数行为都是把H区作为老年代来看的。

Region中分配对象:不管新生代,还是老年代,创建的对象都要分配在region中,region中分配对象采用的是指针碰撞(Bump the pointer)技术,前面已介绍该技术。并且对于多线程并发操作的region,也可以在该region中使用TLAB技术,为每个线程创建一块线程私有的region空间。

Region记忆集(Remembered Set)
前面已介绍过,对于分代垃圾回收标记对象引用时,在标记新生代对象时,可以被GC ROOTS对象引用的则为有用对象,反之为垃圾对象,但是在指定GC ROOTS时,也需要把老年代对象也加入到GC ROOTS,因为老年代对象中也有可能会引用新生代中的对象,被老年代中引用的对象也会被标记为有用对象。同样对于G1垃圾回收器,需要回收的是一个个的region,但是一个region不一定是孤立的,比如一个新生代中的region中的对象可能被其它region中对象引用,判断这个region中对象存活时,难道还要把其余所有的老年代region(主要回收新生代时,只需要把老年代的region加入GC ROOTs)都加入GC ROOTS,显然会降低Minor GC的效率。
为了解决上述问题,采用了region的记忆集,解决方法如下:
无论时G1还是其它分代垃圾回收器,JVM都是使用了Remembered Set方式避免的全堆扫描,对于G1垃圾回收来说,每一个region都有一个对应的Remembered Set,假设新生代一个region中有对象A,每当老年代region中有引用A的对象写入时,就会产生一个write barrier暂时中断的内存屏障,此时检查引用A的对象所在的region是否与对象A所在的region在同一个region中,如果不在同一个region中,就会把这个老年代region中引用A的对象的地址加入到A对象所在region对应的Remembered Set,如下图所示。如果这两个引用对象都在一个region中,就没必要加入Remembered Set了,因为G1是按region进行回收的,如果没有GC ROOTS来引用该region中的这两个对象,这两个对象是会被一起回收的。当要进行垃圾回收时,GC枚举的范围加入Remembered Set就可以了,就可以保证不进行全局扫描,也不会有遗漏对象。比如对一个region进行标记对象,就会把这个region对应的Remembered Set加入到GC ROOTS中。
在这里插入图片描述


3.8.7 G1垃圾回收器的主要过程

G1垃圾回收器的垃圾回收过程主要分为如下几个环节,如下图所示:

  1. 年轻代GC(Young GC)
  2. 老年代并发标记过程(Concurrent Marking)
  3. 混合回收(Mixed GC)
  4. 单线程、独占式的Full GC,Full GC不是每个过程周期都需要执行的。比如设置的暂停时间太短,导致每次年轻代GC和混合回收的对象慢于新创建的对象,此时就要Full GC,否则就要内存溢出了。
    在这里插入图片描述
    G1垃圾回收器是按照上面4个环节依次执行的。应用程序执行时会创建新对象,当年轻代的Eden区用完时开始年轻代的垃圾回收,G1的年轻代垃圾回收是一个并行的独占式垃圾回收,在年轻代回收期间,G1会暂停用户线程,年轻代回收内存时,存活对象从Eden区移到Survivor区或者Old区(大对象,Survivor区放不开时,直接回收到老年代),或者两个区都涉及。
    当堆内存使用达到默认值45%时,开始老年代并发标记过程,与用户线程同时或交替执行。
    并发标记结束后开始混合回收过程,混合回收时,对于老年代不需要回收整个老年代内存空间,一次只需要扫描一小部分老年代Region区域就可以了,然后回收这一部分Region,老年代的回收与新年代回收是一起执行的,因为老年代垃圾回收都会伴随着新年代的垃圾回收。
    下面分别介绍4个阶段的具体执行过程:

1. 阶段一 年轻代GC
应用线程执行时,创建好的对象先分配到Eden区,当Eden区耗尽时(注意Survivor区耗尽时不会触发年轻代垃圾回收),G1会启动一次年轻代的垃圾回收过程。年轻代的垃圾回收(YoungGC)只会回收Eden和Survivor区。YoungGC时,G1会暂停用户线程执行。如下图所示
在这里插入图片描述

YoungGC 时,有如下情况,Eden区的region和Surivivor区的region通过复制算法复制到一个Surivivor区的region中,然后复制前的几个region被回收;还有Surivivor区的region中对象的年龄达到阈值,晋升到老年代中。

年轻代具体垃圾回收过程又分为如下步骤:
第一:扫描GC ROOTS,前面已介绍过哪些对象可以作为GC ROOTS根对象,除了前面介绍的集中GC ROOTS,还要把记忆卡RSet中记录的老年代对象也要加入到GC ROOTS中;
第二:更新RSet,用户程序执行时,如有老年代引用了年轻带对象时会先记录到一个dirt card queue队列中,在此阶段把引用关系更新到Rset中;
第三:识别老年代引用年轻代的对象(通过Rset),这些对象被认为存活对象;
第四:复制对象,Eden区的存活的对象被复制到Survivor区中空闲的Region中,Survivor的region中存活的对象如果年龄未达到阈值,年龄加1,达到阈值的就会被复制到Old区空闲的region中。如果Survivor空间不够,Eden区部分存活的对象会被直接晋升到老年代中;
第五:处理引用关系,处理Soft、Weak、Phantom、Final、JNI Weak等引用,最终Eden区的空间被清空,堆中空间都是整理过的,GC停止工作。

2. 阶段二:并发标记过程
老年代的表发标记阶段又可以分为如下步骤:
第一:标记从根节点直接可达对象,此阶段是STW的,并且会触发一次年轻代的GC;
第二:根区扫描(Root Region Scanning),扫描Survivor区直接可达老年代对象的区域,并被标记为引用对象,这一过程必须在Young GC之前完成;
第三:并发标记(Concurrent Marking),在整个堆上并发标记,和应用线程并发执行,此过程可能被YGC中断。在并发标记过程 中,如果region中对象都是垃圾,该region会被立即回收。同时并发标记过程中,会计算每个region中存活对象比例;
第四:再次标记,由于与用户线程并发执行,此步骤需要修正上一次的标记结果;
第五:独占清理,计算各个区域存活对象比例,并进行排序,为下一步骤铺垫,本步骤是STW的;
第六:并发清理阶段,识别并清理完全空闲的区域。

3. 阶段三:混合回收
当越来越多对象晋升到老年代region时,为避免堆内存耗尽,虚拟机会触发一个混合垃圾回收器,即Mixed GC。Mixed GC会回收整个Young Region,还会回收一部分Old Region(注意不是全部老年代)。需要注意的是Mixed GC不是Full GC。如下图所示
在这里插入图片描述
从图中可以看出,存在着年轻代的垃圾回收,Eden区或者Survivor区中region中存活对象复制到Survivor区中空闲的region中。另外还存在着老年代垃圾回收,Old region中或者Survivor region中存活对象复制到空闲的Old region中。混合回收具体回收细节如下:

  • 并发标记结束后,老年代中百分百为垃圾的region被回收了,部分为垃圾对象的region被计算出了对象存活比。默认情况下,老年代region被分为8次(可以通过-XX:G1MixedGCCountTarget)被回收;
  • 混合回收会回收所有年轻代垃圾对象(包括Eden区和Survivor区中的region中垃圾对象)以及老年代中1/8的region。年轻代和老年代回收均采用复制算法;
  • 由于老年代region默认分8次回收,G1会首先回收垃圾多的region,垃圾占比越高的region越先被回收。并且有一个参数阈值会决定region是否要被回收,即-XX:G1MixedGCLiveThresholdPercent,默认为65%,意思垃圾占比达到65%的region才会被回收。如果垃圾占比太低,意味着存活对象占比高,复制的时候会花费更多时间。
  • 混合回收并不一定要求进行8次,有一个阈值-XX:G1HeapWastePercent,默认值10%,意思允许整个堆中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存比例低于10%,则不再进行混合回收。因为GC会花费很多时间但回收到的内存却很少。

阶段四:FULL GC
G1的初衷是要避免FULL GC的出现,但如果前面几步不能正常工作时,G1就会Stop The World,进行FULL GC,使用单线程的内存回收算法进行垃圾回收,性能会非常差,用户线程停顿时间会很长。

G1什么情况下会出现FULL GC的情况呢?比如当堆内存太小,当G1复制存活对象时没有空的region可用,则会进行FULL GC,如增大内存可解决此类问题。
导致FULL GC的原因可能有如下两个:

  1. 没有空的region用于复制存活对象;
  2. 并发标记未完成之前,用户线程创建对象太快,导致空间耗尽。

3.8.8 G1垃圾回收器优化建议
可以从以下2个方面进行优化

  1. 年轻代大小
    避免使用-Xmn或-XX:NewRatio等相关选项显示设置年轻代大小;
    固定年轻代大小会覆盖暂停时间目标。
  2. 暂停时间目标不要太过严苛
    G1垃圾回收器的吞吐量目标时90%的应用程序时间和10%的垃圾回收时间;
    评估G1的吞吐量时,暂停时间不要设置的太严苛。暂停时间设置太小的话,每次回收的内存空间也少,垃圾回收的频率就会上升,会影响系统的吞吐量。

3.8.9 7种经典垃圾回收器对比
截止JDK8,7种经典垃圾回收器对比如下
在这里插入图片描述

3.9 其他垃圾回收器

1. Shenandoah垃圾回收器
在JDK12(OpenJDK)时,引入了Shenandoah GC,该垃圾回收器特点是低延迟。Shenandoah 是red hat开发的,与上面介绍的7个经典垃圾回收器比,该垃圾回收器是唯一没有进orcale JDK的,其他几个都是oracle官方的。
Shenandoah 团队对外宣称,Shenandoah GC的回收暂停时间与堆大小无端,暂停时间可以控制在10ms以内。但实际应用与官方宣称有出入。

2. ZGC垃圾回收器
ZGC是在JDK11时提出来的,与Shenandoah GC的目标相似,都是在尽可能对吞吐量影响不大的情况下,实现任意堆内存大小下都可以把垃圾收集的停顿时间限制在10ms以内的低延迟。
ZGC基于region内存布局,不设分代,使用并发的标记-压缩算法,以低延迟为首要目标。ZGC几乎在所有地方都可以并发执行,除了开始标记的STW,所以ZGC的停顿时间几乎就耗费在了初始标记的上,这部分的耗时是非常少的。
目前ZGC还处于试验阶段,但性能强悍,未来将是服务端、大内存、低延迟应用的首选垃圾回收器。


参考《深入理解java虚拟机》

这篇关于5. JVM垃圾回收的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!