[TOC]

查询默认垃圾收集器

java -XX:+PrintCommandLineFlags -version

结果如下:

-XX:InitialHeapSize=257798976 
-XX:MaxHeapSize=4124783616
-XX:+UseCompressedClassPointers 
-XX:+UseCompressedOops 
-XX:-UseLargePagesIndividualAllocation 
-XX:+UseParallelGC
-XX:+PrintCommandLineFlags
java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)

可以看到 jdk1.8 使用的ParallelGC , Parallel 垃圾收集器,即 PS Scavenge(Parallel Scavenge) 和 PS MarkSweep( Parallel Old)。

注意这里的 + 号,有 +/- 号表示启用/关闭,没有的表示是配置属性。

JDK8 及以前使用的是Parallel 垃圾收集器,jdk9 到 jdk16使用的都是 G1 收集器。

虽然推出了 ZGC, 但 可能是由于稳定性考虑没有马上更换

CMS默认情况下没有用在任何版本的回收器, CMS 使用标记清除算法,如果用作默认确实不怎么合适。

【Java】JVM - 各版本默认垃圾收集器 - 掘金 (juejin.cn)

【Java】JVM - 垃圾收集器 - 掘金 (juejin.cn)

彻夜研究了2天终于知道 JDK8 默认的GC 收集器了 - 掘金 (juejin.cn)

ZGC

ZGC 很牛逼,它的目标(特性)是:

  • 停顿时间不超过 10ms;
  • 停顿时间不会随着堆的大小,或者活跃对象的大小而增加;
  • 支持 8MB~4TB 级别的堆,未来支持 16TB。

与CMS中的ParNew和G1类似,ZGC也采用标记-复制算法,

不过ZGC对该算法做了重大改进:ZGC在标记、转移和重定位阶段几乎都是并发的,这是ZGC实现停顿时间小于10ms目标的最关键原因。

ZGC通过染色指针和读屏障技术,解决了转移过程中准确访问对象的问题,实现了并发转移

应用线程访问对象将触发“读屏障”,如果发现对象被移动了,那么“读屏障”会把读出来的指针更新到对象的新地址上,这样应用线程始终访问的都是对象的新地址。那么,JVM是如何判断对象被移动过呢?就是利用对象引用的地址,即着色指针。

新一代垃圾回收器ZGC的探索与实践 - 美团技术团队 (meituan.com)

指针染色

一种用于标记对象状态的技术。在一个指针中,除了存储对象的实际地址外,还有额外的位被用来存储关于该对象的元数据信息。这些信息可能包括:

  • 对象是否被移动了(即它是否在回收过程中被移动到了新的位置)。
  • 对象的存活状态。
  • 对象是否被锁定或有其他特殊状态。

通过在指针中嵌入这些信息,ZGC 在标记和转移阶段会更快,因为通过指针上的颜色就能区分出对象状态,不用额外做内存访问。

读屏障

当程序尝试读取一个对象时,读屏障会触发以下操作:

  • 检查指针染色:读屏障首先检查指向对象的指针的颜色信息。

  • 处理移动的对象:如果指针表示对象已经被移动(例如,在垃圾回收过程中),读屏障将确保返回对象的新位置。

  • 确保一致性:通过这种方式,ZGC 能够在并发移动对象时保持内存访问的一致性,从而减少对应用程序停顿的需要。

    读屏障也解决了三色标记法中的漏标/错标的问题

深入理解 JVM 的垃圾收集器:CMS、G1、ZGC | 二哥的Java进阶之路

G1

G1(Garbage First)垃圾回收器不同于其他几款垃圾回收器,其采用独特的内存管理策略,实现对整个堆空间的垃圾回收。

G1垃圾回收器主要将堆内存划分为多个大小相等的区域(称为Region),各个区域根据需要扮演不同的角色,可以被定义为Eden区、Survivor区、Old区和Humongous区(存放大对象,老年代的一部分),采用复制算法针对每个区域进行垃圾回收,同样也支持动态的调整内存大小。同时各个Region不需要连续的存储,颠覆了以往堆内存结构的连续性,具备强大的灵活性,也进一步提高了内存利用率。

G1垃圾回收器在设计时,充分结合了Parallel Scavenge和CMS垃圾回收器的优点,解决Parallel Scavenge和CMS存在的问题,G1也称垃圾优先回收器,不会像其它垃圾回收器一样等到空间快满时才会进行回收,而是提前触发回收(少量多次),每次垃圾回收时间短,吞吐量高,

1. G1垃圾回收模式

在G1垃圾回收器中,主要采用了两种垃圾回收模式:Young GC和Mixed GC

Young GC(年轻代回收):主要针对年轻代区域的垃圾回收,包括Eden区和Survivor区。当所有Eden区使用率达到最大阀值(默认60%)或者G1计算出来的回收时间接近用户设定的最大暂停时间时,会触发一次Young GC,回收Eden区和Survivor区,复制移动到另外的Survivor幸存者(年龄+1)或Old老年代区(提前晋升的) Mixed GC(混合回收):Mixed GC是G1垃圾回收器独有的,也称混合回收,针对年轻代和部分老年代区域的垃圾回收。当老年代的占有率达到阀值(默认45%)或年轻代被分配大对象时,会触发一次Mixed GC,回收所有年轻代和一部分老年代区(选取的策略是垃圾对象最多的老年代区域,确保释放更多内存空间,即回收价值高的),控制最大暂停时间。

2. 年轻代回收(Young GC)原理

G1垃圾回收器年轻代回收时,采用了三种关键技术,分别是记忆集、卡表和写屏障。接下来我们层层递进,研究一下这些技术分别解决了什么问题。

当G1触发Young GC时,只会扫描年轻代区域(Eden区 + Survivor区)的对象,从GC Root根对象出发时,很容易扫描出年轻代的对象以及年轻代对象引用的其它年轻代的对象。

但这样会产生一个问题,如果年轻代的对象被老年代的对象引用了,应该如何识别出来呢?

2.1 记忆集(RememberedSet)

其实G1垃圾回收器内部维护了一种引用详情表,称为记忆集的数据结构,记录了非回收区域(老年代)对象引用回收区域(年轻代)对象的关系。

记忆集仅记录跨代的对象引用关系,不会记录年轻代区域之间的对象引用

在Young GC回收年轻代对象时,会将记忆集中的对象也加入到GC Root中,避免年轻代的对象被错误的回收

java基础-记忆集 | 米二

2.2 卡表(Card Table)与卡页(Card Page)

在G1垃圾回收器中,为了进一步压缩记忆集占用的内存,其将所有的Region区域按大小划分为多个分块,称为卡页(Card Page),对每个卡页进行编号

同时每个Region区域都会有额外配备一小块内存,这块内存称为卡表(Card Table),用于记录整个堆空间中有哪些卡页引用了自己Region区域的对象,卡表的底层数据结构是字节数组,每一个字节对应一个卡页,当某个卡页中的对象引用自己Region区域的对象时,会将卡表对应编号位置的字节修改为1,为1的字节被称之为脏卡

此时生成记忆集就会比较容易,只用遍历各个Region的卡表,找到所有字节为1的脏卡,形成记忆集。

当年轻代垃圾回收标记存活对象时,G1将此记忆集中的所有对象也加入到GC Root根对象集合中,确保被老年代引用的年轻代对象标记为存活。

2.3 写屏障(Write Barrier)

更新卡表状态的底层采用了写屏障技术(具体为写后屏障),当执行对象引用相关的代码时,会在其代码前后插入对应的指令

写屏障类似与之前学过的AOP,会在引用对象赋值前后做一些额外的动作,主要分为两个:

写前屏障:引用对象赋值前的特殊处理 写后屏障:引用对象赋值后的特殊处理 JVM中的写屏障要与并发乱序执行中的内存屏障不一样,这里要区分它们

写后屏障指令判断到老年代对象引用年轻代对象时,会更改卡表中对应的字节为脏卡,同时会将脏卡放入到一个脏卡队列中,JVM会通过单独的线程,定期读取脏卡队列中的数据,更新记忆集

可能会有小伙伴会产生好奇,在更新完卡表之后,为何不直接把脏卡写入记忆集呢?

这是由于写屏障指令是由用户线程完成的,如果有大量的用户线程修改对象引用关系,会产生线程安全问题,则需要对记忆集进行加锁,加锁之后势必会影响执行效率。

因此这里将脏卡先放入脏卡队列,采用单独的线程异步消费,避免影响用户线程

3、混合回收(Mixed GC)原理

混合回收是针对年轻代和部分老年代区域的垃圾回收,当老年代内存占用率达到设定阈值,或分配大对象时,将会触发混合回收Mixed GC

由于Old老年代区往往存在较多对象,G1垃圾回收器为提升执行效率,减少STW,部分耗时较长的阶段采用了与用户线程并发执行,总体分为初始标记、并发标记、最终标记、清理和转移等五个阶段

只有并发标记不会暂停用户线程, (即不会出现STW)

同时为进一步提升处理速度,以及解决并发阶段可能存在的对象引用变化问题,采用三色标记和SATB技术

简述各阶段的作用

  1. 初始标记: 标记GCroot下直接关联的对象

  2. 并发标记: 扫描初始标记中对象的关联对象以及关联链条

  3. 最终标记: 处理并发标记的漏标的对象

  4. 清理: 清除无用的对象

  5. 转移: 转移存活对象, 使内存空间变得连续(region之间转移)

3.1 初始标记阶段

初始标记阶段G1垃圾回收器会暂停用户线程,通过极短的时间仅标记出GC Root根对象直接关联的对象,速度较快。

使用了三色标记法, 三色指的的黑白灰,简单的说,将所有对象渲染成不同的颜色,便于区分。

白色(垃圾对象):白色代表该对象不在GC Root的引用链上,在标记开始时,堆内存中的所有对象默认都是白色,当标记结束,如果对象仍然为白色,则被认为是垃圾对象 灰色(待处理对象):灰色代表该对象在GC Root的引用链上,但该对象所有引用的对象还未被标记过,是一个过渡颜色,最终会被标记为黑色 黑色(存活对象):黑色代表该对象在GC Root的引用链上,且该对象所有引用的对象均已被标记过,代表存活对象

CMS垃圾回收器也使用到了三色标记算法

java常见面试题 -三色标记法| 米二

3.2 并发标记阶段

并发标记阶段从GC Root直接关联的对象进行完整遍历,标记出所有存活的对象,耗时较长,但本阶段与用户线程可同时并发执行,不会暂停用户线程。

在本阶段G1垃圾回收器会从灰色队列中获取到未标记完成的灰色对象,标记其引用关联的所有下一级对象(结束后自身会被标记为黑色),将其引用的下一级对象标记为灰色,放入灰色队列,重复这一过程,直到队列为空

但是,并发标记阶段由于用户线程并没有暂停,会产生新问题,即用户线程若在此期间修改了对象的引用关系,就会导致标记结果不准确,这里其实会误判两种情况:多标和漏标

  • 多标:即本应该被回收的垃圾对象,却被标记为黑色存活对象。当已被标记的黑色对象或灰色对象,其引用被删除,就会造成多标

  • 漏标:即本应该被标记为黑色的存活对象,却没有被正常标记为黑色,被垃圾回收。当未处理完的灰色对象断开了它引用的白色对象,且已处理完的黑色对象重新引用了该白色对象,则该白色对象会被漏标

多标的问题其实并不严重,顶多产生浮动垃圾,等到下一次垃圾回收时也会被回收,但漏标却是很致命的,已经影响到了程序的正常运行,而G1垃圾回收器采用了SATB原始快照技术解决了这一漏标问题

1)SATB(Snapshot At The Beginning)

SATB主要为解决并发标记阶段可能产生的对象引用变化问题,SATB即一个原始快照,类似于拍照一样,记录某一时刻所有的对象,

可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。

3.3 最终标记阶段

最终标记阶段会暂停用户线程,主要用于修正并发标记期间产生变动的对象,总体耗时短于并发标记阶段。

最终标记阶段首先会暂停用户线程,将所有线程的SATB队列合并到全局SATB队列,逐一消费。

在全局SATB队列中的对象,默认按照黑色存活对象处理,同时处理它们引用的其它对象。

缺点:显而易见,SATB也会造成多标的情况,将可以被回收的垃圾对象标记为存活对象,产生了浮动垃圾,这些浮动垃圾需要等到下一轮垃圾回收时被回收。

3.4 清理阶段

清理阶段也会暂停用户线程,在最终标记阶段完成之后,G1垃圾回收器会整理Region区域,调整对应的记忆集,若识别到某个Region不存在任何存活对象时,会直接清理掉该Region,释放内存

3.5 转移阶段

转移阶段同样也会暂停用户线程,需要将某一个Region区域存活的对象复制到另一个Region,主要包括以下步骤:

  1. 区域选择:G1垃圾回收器会针对各个Region的回收价值进行排序,评估最大暂停时间,选择特定的Region进行回收(一般是垃圾对象最多的区域)

  2. 对象复制转移:选定好Region后,首先会将GC Root根对象直接关联的对象转移到新的Region区域,然后依次转移其引用的对象

  3. 更新引用关系:对象转移到新的Region区域后,会清理到之前的Region。若其它Region区域中的对象引用了转移后的对象,则会重新设置它们的引用关系,避免对象移动位置后引用出错

深入JVM:详解G1垃圾回收器原理-CSDN博客

CMS

CMS存在的问题

  1. 使用的标记-清除算法,可能存在大量空间碎片。
  2. 并发清理, 内存不够会出现Concurrent Mode Failure失败而导致垃圾收集器降级为Serial Old, 产生STW
  3. 对CPU资源非常敏感。在并发阶段,会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4

CMS收集器的工作流程(步骤)是什么样的?

  1. 初始标记: 只标记和GC Roots能直连的对象,速度快,会发生(stop the world)
  2. 并发标记: 和应用线程并发执行,遍历初始标记阶段标记过的对象,标记这些对象的可达对象。
  3. 重新标记: 由于并发标记是和应用线程是并发执行的,所以有些标记过的对象发生了变化。这个过程比初始标记用时长,但是比并发标记阶段用时短。会发生(stop the world)
  4. 并发清除: 和应用线程一起运行。基于标记对象,直接清理对象。

总结CMS常见面试题_入门小站的博客-CSDN博客_cms面试题

关于CMS收集器的问题? - 知乎 (zhihu.com)

JVM 面试必问的 CMS,你懂了吗?_web13618542420的博客-CSDN博客