JVM垃圾回收机制

彻底认识Jvm堆

Posted by ALID on December 12, 2019

堆内存

想要垃圾回收就必须知道JVM堆内存的设计

img

JVM大致可以分为这几块, 设计到垃圾回收的主要是堆的部分

img

在传统GC内存布局中JVM的堆内存被分为新生代,老年代. 以及在本地内存中的元空间. 这些部分都在垃圾回收器的掌控之中.

img

怎么找到被回收对象

引用计数

虽然简单, 但无法解决循环引用的问题

可达性分析

GC Roots

主要就是全局引用的和正在执行的

  • 虚拟机栈(栈桢中的本地变量表)中引用的
  • 方法区中类静态变量属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的
  • 新生代回收的时候还要遍历卡表

强软弱虚

  • 强 不会被回收 [直接等号赋值 99.999%都是强引用]
  • 软 内存不够的时候回收 [可以用来做缓存]
  • 弱 下次GC回收 [ThreadLoacl]
  • 虚 立刻回收 [当发生GC,虚引用就会被回收,并且会把回收的通知放到ReferenceQueue中。虚引用有什么用呢?在NIO 中,就运用了虚引用管理堆外内存。在使用NIO的对象被回收后, 就可以通过这个通知去把他引用的堆外内存 回收掉]

也不是非死不可 如果finalize()方法中可以关联其他对象则不会被回收

方法区回收 - 该类所有的实例已经被回收(堆中不存在任何该类的实例) - 加载该类的ClassLoader已经被回收 - 该类对应的java.lang.Class对象在任何地方没有被引用(无法通过反射访问该类的方法)

安全点/安全区 只有到安全点的时候才会STW并开始GC, 一般在方法调用, 循环跳转等执行相对较慢的地方设立安全点. 除此之外对于线程在sleep或blocked状态的情况, 需要设立安全区. 线程在安全区中的时候会开始枚举根节点, 直到完成才可以走出根节点.

Card Table

记录了新生代被老年代引用的记录

在JVM在进行垃圾收集时,需要先标记所有可达对象. 但堆空间通常被划分为新生代和老年代。由于新生代的垃圾收集通常很频繁,如果老年代对象引用了新生代的对象,那么,需要跟踪从老年代到新生代的所有引用,从而避免每次YGC时扫描整个老年代,减少开销。

这样新生代在GC时,可以不用花大量的时间扫描所有年老代对象,来确定每一个对象的引用关系,而可以先扫描卡表,只有卡表的标记位为1时,才需要扫描给定区域的年老代对象。而卡表位为0的所在区域的年老代对象,一定不包含有对新生代的引用。

卡表是个单字节数组,每个数组元素对应堆中的一张卡。

img

写屏障

写屏障可以理解为是对于对引用类型字段赋值的一个切面, 也就是在发送对引用类型字段赋值的时候, 就会去做一些操作.

每次年老代对象中某个引用新生代的字段发生变化时,Hotspot VM就必须将该卡所对应的卡表元素设置为适当的值,从而将该引用字段所在的卡标记为脏。在Minor GC过程中,垃圾收集器只会在脏卡中扫描查找年老代-新生代引用。 img

Hotspot VM的字节码解释器和JIT编译器使用写屏障维护卡表。写屏障是一小段将卡状态设置为脏的代码。解释器每次执行更新引用的字节码时,都会执行一段写屏障,JIT编译器在生成更新引用的代码后,也会生成一段写屏障。虽然写屏障使得应用线程增加了一些性能开销,但Minor GC变快了许多,整体的垃圾收集效率也提高了许多,通常应用的吞吐量也会有所改善。

回收策略

对象优先进入新生代 如果young gen中的eden区没有足够的空间则会发现则会触发Minor GC

大对象直接进入老年代 -XX:PrerenureSizeThreshold 默认是0, 所有对象都先进入Eden区. 大于该参数设置的值, 直接进入老年代

长期存活对象进入老年代 -XX:MaxTenuringThreshold 默认15, 存活过15次 Minor GC 的对象会进入老年代

动态对象年龄判定 如果相同年龄的对象大小总和大于幸存者区空间的一半, 则大于等于该年龄的对象都进入老年代

空间分配担保 在Minor GC之前, 会校验老年代的连续空间是否大于新生代所有对象的总和空间. 如果HandlePromotionFailure设置为容许, 则会再次判断是否大于历次晋升到老年代的平均大小. 如果不够或者刚刚的设置是不允许则会触发一次Full GC

这里是因为在极端情况下会出现新生代对象基本都存活. 那样就需要老年代进行分配担保, 把幸存者去无法容纳的对象移到老年代. 如果在这里移到失败, 会发生一次Full GC来腾出空间.

触发条件

Young GC

各种Young GC发生的原因都是eden区被占满

Parallel Scavenge(-XX:+UseParallelGC)框架下,默认是在要触发full GC前先执行一次young GC,并且两次GC之间能让应用程序稍微运行一小下,以期降低full GC的暂停时间(因为young GC会尽量清理了young gen的死对象,减少了full GC的工作量)

非并发full GC

Serial Old GC/PS MarkSweep GC/Parallel Old GC 的触发则是在要执行Young GC时候预测其promote的object的总size超过老生代剩余size

大对象直接放入老年代, 且老年代当前空间不足的时候

并发收集引擎

  • CMS GC的initial marking的触发条件是老生代使用比率超过某值
  • G1 GC的initial marking的触发条件是Heap使用比率超过某值
  • Full GC for CMS算法和Full GC for G1 GC算法的触发原因很明显, 就是他们触发老年代的回收赶不上分配内存的速度.

G1

G1是并发,整理,混合的垃圾回收器, 在Jdk1.9中作为默认的垃圾回收算法.

特点

  1. 它是专门针对以下应用场景设计的:
    • 像CMS收集器一样,能与应用程序线程并发执行。
    • 整理空闲空间更快。
    • 需要GC停顿时间更好预测。
    • 不希望牺牲大量的吞吐性能。
  2. G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色:
    • G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。
    • G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。
  3. Remembered Set: 每个区块都有一个 RSet,用于记录进入该区块的对象引用(如区块 A 中的对象引用了区块 B,区块 B 的 Rset 需要记录这个信息),它用于实现收集过程的并行化以及使得区块能进行独立收集。总体上 Remembered Sets 消耗的内存可能会大于 10%。G1 的 RSet 是在Card Table的基础上实现的.

  4. Collection Set: 它记录了GC要收集的Region集合,集合里的Region可以是任意年代的。在GC的时候,对于old->young和old->old的跨代对象引用,只要扫描对应的CSet中的RSet即可。GC 时,在这些区块中的对象会被复制到其他区块中,总体上 Collection Sets 消耗的内存小于 1%。

结构

-XX:+UseG1GC 设置G1垃圾回收器
-XX:NewRatio=n 老年代/年轻代,默认值 2,即 1/3 的年轻代,2/3 的老年代
-XX:SurvivorRatio=n Eden/Survivor,默认值 8,这个和其他分代收集器是一样的
-XX:MaxTenuringThreshold =n 从年轻代晋升到老年代的年龄阈值,也是和其他分代收集器一样的
-XX:G1HeapRegionSize=n 每一个 region 的大小(默认2048个),默认值为根据堆大小计算出来,取值 1MB~32MB,这个我们通常指定整堆大小就好了。

相较于传统的垃圾回收器, G1的数据结构也发生了变化, G1的各代存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region,每个Region占有一块连续的虚拟内存地址。 img 在上图中,我们注意到还有一些Region标明了H,它代表Humongous,这表示这些Region存储的是巨大对象(humongous object,H-obj),即大小大于等于region一半的对象。H-obj有如下几个特征:

  • H-obj直接分配到了old gen,防止了反复拷贝移动。
  • H-obj在 global concurrent marking 阶段的cleanup 和 full GC阶段回收。
  • 在分配H-obj之前先检查是否超过 initiating heap occupancy percentthe marking threshold, 如果超过的话,就启动 global concurrent marking,为的是提早回收,防止 evacuation failuresfull GC

为了减少连续H-objs分配对GC的影响,需要把大对象变为普通的对象,建议增大Region size。 一个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围从1M到32M,且是2的指数。如果不设定,那么G1会根据Heap大小自动决定

而对于超过整个Region区的超级大对象, 会被放入几个连续的Humongous区中

全局并发标记(global concurrent marking)

-XX:InitiatingHeapOccupancyPercent 参数控制启动并发标记周期, 此参数默认值是 45,也就是说当堆空间使用了 45% 后,G1 就会进入并发标记周期。

并发标记的过程

初始标记

stop-the-world,它往往伴随着一次普通的Young GC发生,然后对Survivor区(root region)进行标记,因为该区可能存在对老年代的引用。

因为 Young GC 是需要 stop-the-world 的,所以并发周期直接重用这个阶段,虽然会增加 CPU 开销,但是停顿时间只是增加了一小部分。就是在标记了从GC Root开始直接可达的对象

并发标记

使用STAB进行标记

这个阶段是并发执行的,中间可以发生多次Young GC,Young GC 会中断标记过程. 就是使用可达性分析来标记

重新标记

stop-the-world,完成最后的存活对象标记。标记那些在并发标记阶段发生变化的对象,将被回收。

并发标记阶段, GC 线程完成对象图的扫描之后,还会去处理 SATB 记录下的在并发时有引用变动的对象。 在处理 SATB 记录的数据的时候,由于用户线程可能还是在继续修改对象图,继续在产生新的 SATB 数据,所以还是会有一小部分的 SATB 数据,所以才需要一个短暂的暂停。

这里虽然用的是使用原始快照的方法, 但是对于在并发标记阶段新产生的对象, 还是要重新标记的.

清除垃圾(Cleanup)

清除空Region(没有存活对象的),加入到free list。 这里并不会去选择标记的垃圾进行清理.

SATB算法

全称是Snapshot-At-The-Beginning,由字面理解,是GC开始时活着的对象的一个快照。它是通过Root Tracing得到的.

那么它是怎么维持并发GC的正确性的呢?根据三色标记算法,我们知道对象存在三种状态:

  • 白:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉。
  • 灰:对象被标记了,但是它的field还没有被标记或标记完。
  • 黑:对象被标记了,且它的所有field也被标记完了。

由于并发阶段的存在,Mutator和Garbage Collector线程同时对对象进行修改,就会出现白对象漏标的情况,

这种情况发生的前提是:

  • Mutator赋予一个黑对象该白对象的引用。
  • Mutator删除了所有从灰对象到该白对象的直接或者间接引用。

对于第一个条件,在并发标记阶段,如果该白对象是new出来的,并没有被灰对象持有,那么它会不会被漏标呢?Region中有两个top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS。在TAMS以上的对象是新分配的,这是一种隐式的标记。对于在GC时已经存在的白对象,如果它是活着的,它必然会被另一个对象引用,即条件二中的灰对象。如果灰对象到白对象的直接引用或者间接引用被替换了,或者删除了,白对象就会被漏标,从而导致被回收掉,这是非常严重的错误,所以SATB破坏了第二个条件。也就是说,一个对象的引用被替换时,可以通过write barrier 将旧引用记录下来。

SATB也是有副作用的,如果被替换的白对象就是要被收集的垃圾,这次的标记会让它躲过GC,这就是float garbage。因为SATB的做法精度比较低,所以造成的float garbage也会比较多。

使用了bitmap+指针的方式标记

img img

img

PrevBitmap 这个 Region 由于“价值”不够,它逃过了上次垃圾回收,所以待到下次垃圾回收的时候,就是 prevBitmap 的用武之地了,它里面记录的地址对应的区间就不需要再次标记了,因为这些地址对应的对象就已经是垃圾了。

漏标与浮动垃圾

这里需要再看一下在并发标记中所使用到的三色标记算法

白色:表示对象尚未被垃圾回收器访问过。显然,在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。 黑色:表示对象已经被垃圾回收器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其它的对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。 灰色:表示对象已经被垃圾回收器访问过,但这个对象至少存在一个引用还没有被扫描过

读完上面描述,再品一品下面的图: img

可以看到,灰色对象是黑色对象与白色对象之间的中间态。当标记过程结束后,只会有黑色和白色的对象,而白色的对象就是需要被回收的对象。 以上的情况我们只考虑了, 当前只有垃圾回收线程工作的情况, 但并发标记的时候用户线程还在工作. 这样我们就需要考虑以下内容.

垃圾回收器和用户线程同时运行 一种是把原本消亡的对象错误的标记为存活,这不是好事,但是其实是可以容忍的,只不过产生了一点逃过本次回收的浮动垃圾而已,下次清理就可以。 浮动垃圾是怎么产生的呢? 其实很简单就是在并发标记已经被标记的节点(但没有标记完所有引用节点所以是灰色的), 在之后的程序运行中, 用户线程断开了对其的引用. 另一种是把原本存活的对象错误的标记为已消亡,这就是非常严重的后果了,一个程序还需要使用的对象被回收了,那程序肯定会因此发生错误。

这里就是一个在扫描到的时候是应该被回收的元素, 在之后又被引用了

  1. 赋值器插入了一条或者多条从黑色对象到白色对象的新引用。
  2. 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

这样就有了一下两种方案, 增量更新(Incremental Update)和原始快照(Snapshot At The Beginning,SATB)。 在HotSpot虚拟机中,CMS是基于增量更新来做并发标记的,G1则采用的是原始快照的方式。

原始快照 因为删除操作会触发写前屏障(pre-write barrier),把每次引用关系变化时旧的引用值记下来,只有这样,等 GC 线程到达某一个对象时,这个对象的所有引用类型字段的变化全都有记录在案,就不会漏掉任何在快照图里活的对象。当然,很可能有对象在快照中是活的,但随着并发 GC 的进行它可能本来已经死了,但 SATB 还是会让它活过这次 GC,变成了浮动垃圾。

SATB 在写屏障里,把旧的引用所指向的对象都变成非白的(已经黑灰就不用管,还是白的就变成灰的)。

增量更新 增量更新很简单, 就是会在扫描完成之后再扫描一遍所有新增节点 对于发送变化的对象,需要重新标记以防止被遗漏。为了提高重新标记的效率,本阶段会把这些发生变化的对象所在的Card标识为Dirty,这样后续就只需要扫描这些Dirty Card的对象,从而避免扫描整个老年代。

即停顿预测模型(Pause Prediction Model)

-XX:MaxGCPauseMillis=200

G1 GC是一个响应时间优先的GC算法,它与CMS最大的不同是,用户可以设定整个GC过程的期望停顿时间,参数-XX:MaxGCPauseMillis指定一个G1收集过程目标停顿时间,默认值200ms(通常设到100ms、250ms之类的都可能是合理的, 设到50ms就不太靠谱),不过它不是硬性条件,只是期望值。那么G1怎么满足用户的期望呢?就需要这个停顿预测模型了。G1根据这个模型统计计算出来的历史数据来预测本次收集需要选择的Region数量,从而尽量满足用户设定的目标停顿时间。

GC过程

G1提供了两种GC模式,Young GC和Mixed GC,两种都是完全Stop The World的。

Young GC:选定所有年轻代里的Region。通过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销。 Mixed GC:选定所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region。在用户指定的开销目标范围内尽可能选择收益高的老年代Region。 并且由上面的描述可知,Mixed GC不是full GC,它只能回收部分老年代的Region,如果mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(full GC)来收集整个GC heap。所以我们可以知道,G1是不提供full GC的。 Young GC发生的时机大家都知道(eden区满了),那什么时候发生Mixed GC呢?其实是由一些参数控制着的,另外也控制着哪些老年代Region会被选入CSet。 G1HeapWastePercent:在global concurrent marking结束之后,我们可以知道old gen regions中有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC。 G1MixedGCLiveThresholdPercent:old generation region中的存活对象的占比,只有在此参数之下,才会被选入CSet。 G1MixedGCCountTarget:一次global concurrent marking之后,最多执行Mixed GC的次数。 G1OldCSetRegionThresholdPercent:一次Mixed GC中能被选入CSet的最多old generation region数量。

垃圾回收过程

Evacuation阶段是全暂停的。它负责把一部分region里的活对象拷贝到空region里去,然后回收原本的region的空间。

Evacuation阶段可以自由选择任意多个region来独立收集构成收集集合(collection set,简称CSet),靠per-region remembered set(简称RSet)实现。这是regional garbage collector的特征。

在选定CSet后,evacuation其实就跟ParallelScavenge的young GC的算法类似,采用并行copying(或者叫scavenging)算法把CSet里每个region里的活对象拷贝到新的region里,整个过程完全暂停。从这个意义上说,G1的evacuation跟传统的mark-compact算法的compaction完全不同:前者会自己从根集合遍历对象图来判定对象的生死,不需要依赖global concurrent marking的结果,有就用,没有拉倒;而后者则依赖于之前的mark阶段对对象生死的判定。

论文里提到的纯G1模式下,CSet的选定完全靠统计模型找处收益最高、开销不超过用户指定的上限的若干region。由于每个region都有RSet覆盖,要单独evacuate任意一个或多个region都没问题。 分代式G1模式下有两种选定CSet的子模式,分别对应的young GC与mixed GC, young gen region总是在CSet内。因此分代式G1不维护从young gen region出发的引用涉及的RSet更新。

CMS

-XX:+UseConcMarkSweepGC 使用标记-清除算法并发低停顿老年代收集器
-XX:CMSFullGCsBeforeCompaction 多少次GC后清理一次内存碎片

运行流程

Initial Mark(初始化标记)

初始化标记阶段,是CMS GC的第一个阶段,也是标记阶段的开始。主要工作是标记可直达的存活对象

主要标记过程

  • 从GC Roots遍历可直达的老年代对象;
  • 遍历被新生代存活对象所引用的老年代对象。

程序执行情况

  • 支持单线程或并行标记。
  • 发生stop-the-world,暂停所有应用线程。

img (Marked obj:老年代绿色圆点表示被初始化标记的对象。)

Concurrent Mark(并发标记)

并发标记阶段,是CMS GC的第二个阶段。 在该阶段,GC线程和应用线程将并发执行。也就是说,在第一个阶段(Initial Mark)被暂停的应用线程将恢复运行。 并发标记阶段的主要工作是,通过遍历第一个阶段(Initial Mark)标记出来的存活对象,继续递归遍历老年代,并标记可直接或间接到达的所有老年代存活对象img (Current obj:该对象的引用关系发生变化,对下一个对象的引用被删除。) 由于在并发标记阶段,应用线程和GC线程是并发执行的,因此可能产生新的对象或对象关系发生变化,例如:

  • 新生代的对象晋升到老年代;
  • 直接在老年代分配对象;
  • 老年代对象的引用关系发生变更;

对于这些对象,需要重新标记以防止被遗漏。为了提高重新标记的效率,本阶段会把这些发生变化的对象所在的Card标识为Dirty,这样后续就只需要扫描这些Dirty Card的对象,从而避免扫描整个老年代。

Concurrent Preclean(并发预清理)

在并发预清洗阶段,将会重新扫描前一个阶段标记的Dirty对象,并标记被Dirty对象直接或间接引用的对象,然后清除Card标识

标记被Dirty对象直接或间接引用的对象: img

清除Dirty对象的Card标识: img 有这个步骤的原因是CMS GC的终极目标是降低垃圾回收时的暂停时间,所以在该阶段要尽最大的努力去处理那些在并发阶段被应用线程更新的老年代对象,这样在暂停的重新标记阶段就可以少处理一些,暂停时间也会相应的降低。

Concurrent Abortable Preclean(可中止的并发预清理)

本阶段尽可能承担更多的并发预处理工作,从而减轻在Final Remark阶段的stop-the-world。 在该阶段,主要循环的做两件事:

  • 处理 From 和 To 区的对象,标记可达的老年代对象;
  • 和上一个阶段一样,扫描处理Dirty Card中的对象。

具体执行多久,取决于许多因素,满足其中一个条件将会中止运行:

  • 执行循环次数达到了阈值;
  • 执行时间达到了阈值;
  • 新生代Eden区的内存使用率达到了阈值。

此阶段在Eden区使用超过2M时启动,当然2M是默认的阈值,可以通过参数修改。如果此阶段执行时等到了Minor GC, 就可以减少重新标记的时候扫描新生代而带来的停顿

Final Remark(重新标记)

预清理阶段也是并发执行的,并不一定是所有存活对象都会被标记,因为在并发标记的过程中对象及其引用关系还在不断变化中。

因此,需要有一个stop-the-world的阶段来完成最后的标记工作,这就是重新标记阶段(CMS标记阶段的最后一个阶段)。主要目的是重新扫描之前并发处理阶段的所有残留更新对象

主要工作:

  • [新生代到老年代的引用]遍历新生代对象,重新标记;(新生代会被分块,多线程扫描)
  • [新产生的引用]根据GC Roots,重新标记;
  • [老年代引用发送变化的]遍历老年代的Dirty Card,重新标记。这里的Dirty Card,大部分已经在Preclean阶段被处理过了。

Concurrent Sweep(并发清理)

并发清理阶段,主要工作是清理所有未被标记的死亡对象,回收被占用的空间img

这个阶段性产生的垃圾就无法被清理了, 这些垃圾被称为浮动垃圾

Concurrent Reset(并发重置)

并发重置阶段,将清理并恢复在CMS GC过程中的各种状态,重新初始化CMS相关数据结构,为下一个垃圾收集周期做好准备。如重置卡表.

这里就可以看出CMS和G1在标记阶段最大的不同就是, G1是依赖快照的并不需要太长时间去最终标记; 而CMS是增量更新的, 需要把新变化全部标记到, 就多了2步预清理.

缺点

  1. 比较占用CPU资源, 可能会影响高并发系统功能
  2. 因为要在并发过程中留下足够的内存给用户使用, 这样的话就不能在老年代差不多满的时候再去执行. 如果在执行过程中内存满了的话, 会临时使用serial GC回收.
  3. 因为CMS是标记-清除算法, 所以可能会触发full GC. 但也会在碎片过多的时候开启内存碎片的整理. 我们可以通过设置参数 CMSFullGCsBeforeCompaction 来控制多少次GC后进行一次内存碎片整理.

G1和CMS的对比

  1. 虽然两者都采用了先并行不STW的方法减少停顿, 但是CMS是针对老年代的算法, 而G1可以对老年代和新生代都可以支持
  2. 最大的不同就是G1提供了可配置的期望停顿时间, 使用即停顿预测模型来计算每次GC的块, 而不是一次全部GC
  3. CMS采用标记-清除算法, 会产生内存碎片. 而G1使用的是标记-整理算法

其实CMS在较小的堆、合适的workload的条件下暂停时间可以很轻松的短于G1。在2011年的时候Ramki告诉我堆大小的分水岭大概在10GB~15GB左右:以下的-Xmx更适合CMS,以上的才适合试用G1。现在到了2014年,G1的实现经过一定调优,大概在6GB~8GB也可以跟CMS有一比,我之前见过有在-Xmx4g的环境里G1比CMS的暂停时间更短的案例。

合适的workload:CMS最严重的暂停通常发生在remark阶段,因为它要扫描整个根集合,其中包括整个young gen。如果在CMS的并发标记阶段,mutator仍然在高速分配内存使得young gen里有很多对象的话,那remark阶段就可能会有很长时间的暂停。Young gen越大,CMS remark暂停时间就有可能越长。所以这是不适合CMS的workload。相反,如果mutator的分配速率比较温和,然后给足时间让并发的precleaning做好remark的前期工作,这样CMS就只需要较短的remark暂停,这种条件下G1的暂停时间很难低于CMS。

CMS的incremental update设计使得它在remark阶段必须重新扫描所有线程栈和整个young gen作为root;G1的SATB设计在remark阶段则只需要扫描剩下的satb_mark_queue。

要在拷贝对象的前提下实现真正的低延迟就需要做并发拷贝(concurrent compaction)。但是现在已知的实现concurrent compaction的GC算法无一例外需要使用某种形式的read barrier,例如Azul的C4和Red Hat的Shenendoah。不用read barrier的话,没办法安全的实现一边移动对象一边修正指向这些对象的引用,因为mutator也可以会并发的访问到这些引用。 而G1则坚持只用write barrier不用read barrier,所以无法实现concurrent compaction。

其他几个回收器

Serial 收集器

新时代采用复制算法, 并STW 老年代采用标记-整理, 并STW

Serial old

老年代版本, 单线程, STW 作为CMS的后备方案

parNew

和Serial基本一样, 仅仅是新生代多线程回收 适用于多核(至少4核)的CPU使用

Parallel old

多线程收集

Parallel Scavenge

有自适应策略, 会调整新生代大小来减少停顿时间, 吞吐量优先 但代价是让YGC频率增加 无法与CMS配合

参考

  1. HotSpot VM 请教G1算法的原理 - 资料 - 高级语言虚拟机 - ITeye群组
  2. 面试官:你说你熟悉jvm?那你讲一下并发的可达性分析 - 掘金
  3. 面试官问我G1回收器怎么知道你是什么时候的垃圾? - 掘金
  4. 面试官看了我之前的文章对我说:你回去等通知吧! - 掘金
  5. Java Hotspot G1 GC的一些关键技术 - 美团技术团队
  6. 可能是最全面的G1学习笔记 - 知乎
  7. G1调优常用参数及其作用_Java_点滴之积-CSDN博客
  8. Garbage First Garbage Collector Tuning