JVM 垃圾回收器 G1
G1 收集器是一款面向服务端应用的垃圾收集器,主要针对多 CPU 以及大容量内存的场景,在缩短 STW 的同时,具备高吞吐的特征(大概率)。在启动 JVM 参数加上 -XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200
启用 G1 作为垃圾回收器。
G1具备如下特点:
并行与并发:G1 能更充分的利用多核 CPU,缩短 STW 时间
分区域、分代收集:分代的概念在 G1 中依然存在(但不再是物理上的划分),G1 可以独自管理整个 GC 堆,以区域(region)为管理单位。
空间整合:G1 收集器有利于程序长时间运行,从整体看基于“标记-整理”算法实现,从局部看是基于“复制”算法实现
可预测的非停顿:G1 相对于 CMS 的另一大优势,可建立一个可预测的停顿时间模型,可以指定每次消耗在垃圾收集上的时间不得超过N毫秒。但是会产生另一个问题,垃圾回收的次数会变多。
G1 内存结构
G1 不再按照年轻代年/老代划分堆内存,而是把整个堆空间堆划分成若干个大小相等的内存区域(Region),而在逻辑上一部分区域代表新生代、Survivor 空间,一部分区域代表老年代,新生代的存活对象拷贝到 Survivor 或老年代区域,Survivor 和老年代的存活对象从一块区域拷贝到另一块新的区域,所以不存在碎片问题。
一种特殊的区域,叫 Humongous 区域。如果一个对象占用空间超过了一个分区容量的 50% 以上,G1 就认为这是一个巨型对象。在 CMS 中巨型对象,大概率会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1 划分了一个 Humongous 区,用来专门存放巨型对象。如果一个 H 分区装不下一个巨型对象,那么 G1 会寻找多个连续的 H 分区来存储。为了能找到连续的H区,有时候不得不启动 Full GC。
启动时可以通过参数 -XX:G1HeapRegionSize=n
可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为 2048 个分区。
G1 中的重要数据结构
本地分配缓冲
本地分配缓冲区(Local allocation buffer,Lab)是为了提升分配对象和 GC 的效率而存在的优化算法。不止在 G1 中存在。
TLAB(Thread Local Allocation Buffer)
用于对小对象分配的优化,在 Eden 区中的一块空间(即一个 Region),用于应用线程独占(因此不存在线程间的竞争),应用线程创建的对象会优先使用该区域(巨型对象或分配失败除外)。每个应用线程都有一个 TLAB。
PLAB(Promotion Local Allocation Buffer)
在 YoungGC 时,将全部 Eden 区存活的对象复制到 Survivor 区域,也会存在 Survivor 区对象晋升(Promotion)到老年代(晋升的阈值可通过 -XX:MaxTenuringThreshold=n
设定)。晋升的过程,无论是晋升到 S 还是 O 区,都是在 GC 线程独占的 PLAB 中进行。每个 GC 线程都有一个 PLAB。
Collection Set(CSet)
CSet 是待回收的 Regions 的集合。CSet 中存放着各个分代的 Regions。GC 后 CSet 中的 Regions 会成为可用分区,而存活的对象都会被转移到分配的空闲分区中。
对于 YoungGC,CSet 只包含年轻代的 Regions;对于 MixedGC,CSet 除了年轻代的 Regions,还会通过算法筛选出老年代中回收收益最高的部分 regions。相关参数:
-XX:G1MixedGCLiveThresholdPercent
(默认为一个 Region 分区的 85% 大小),任何一个低于阈值的老年代分区都会被包含在混合收集的 CSet 中。
-XX:G1OldCSetRegionThresholdPercent
(缺省为堆的10%),CSet 包含 Regions 的总大小,占堆的比例不超过阈值。
G1 的收集都是根据 CSet 进行操作的,YoungGC 与 MixedGC 没有明显的不同,最大的区别在于两者的触发条件。
Card Table
在每个 region 内部被分成了若干个大小为 512 Byte 的块叫做卡片(Card),即堆内存最小可用粒度。所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片。
Card Table 的结构是一个字节数组,用单字节的信息映射着一个 Card。当 Card 中存储了对象时,称这个 Card 为 dirty card。
Card table 在 CMS 收集器中也有应用。
Remember Set(RSet)
每个 Region 都会由一个 RSet 维护并跟踪其他 Region 对本 Region 所拥有的引用,也就是存活对象,是一种 points-in 结构。当 Region 中对象被移动时 RSet 也会更新引用。
当 YoungGC 时,通过 RSet 找到引用当前 Region 的 Old 的 Regions 进行扫描。进行 MixedGC 时,同样通过 RSet 找到引用当前 Region 的 Old 的 Regions 进行扫描。避免了扫描全部的 Old 的 Regions,提高扫描效率,并且因为每次 GC 都会扫描所有的年轻代的 Regions,所以查找 RSet 时只需要找到 Old Region 对当前 Region 的引用。
当 Region 被引用较多的情况,RSet 占用空间会上升,因此对 RSet 的记录划分了三种存储粒度:
稀疏表(Sparse):直接通过哈希表来存储,key 是 region index,value 是 card 数组
细粒度(Fine):当一个 region 的 card 数量超过阈值时,为 region 创建一个 PerRegionTable 对象,包含一个 C heap 位图,每一位对应一个 card
粗粒度(Coarse):当 PRT 数量超过阈值时,退化为只记录分区引用情况,由位图存储,每个分区对应一位
SAB(Snaoshot-At-The-Beginning)
SATB 是在 G1 GC 在并发标记阶段使用的增量式的标记算法,解决了 CMS GC 算法中重新标记 STW 时间较长的缺陷。
垃圾回收的并发标记阶段,GC 线程和应用线程是并发执行的,所以一个对象被标记之后,应用线程可能修改了对象的引用关系,从而造成对象的漏标、误标。误标的影响并不严重,可能造成浮动垃圾,在下次 GC 可以被回收;但漏标的后果是致命的,把本应该存活的对象给回收了,影响的程序的正确性。
三色标记法
在了解 SATB 前先了解三色标记法。三色标记法将对象的存活状态用三种颜色标记,从黑色到灰色逐层标记:
黑:根对象,或者该对象以及其引用的对象都被标记完成
灰:该对象被标记了,但其引用的对象还没有全部被标记
白:该对象还没有被标记。标记阶段结束后的白色对象为不可达对象,会被回收。
直观上,这种标记方法不会出现错误,图示:
当垃圾收集器扫描到第二步情况时,应用程序执行
标记结果将会变为:
此时 C 被认为是垃圾需要清理掉,出现了漏标的情况(以上只是其中一种情况)。那么如何避免这种情况呢?
CMS 采用的是增量更新(Incremental update),在增加引用时的写屏障(write barrier)里发现有一个"白"对象的引用被赋值到一个"黑"对象的字段里,那就把这个"白"对象变成"灰"的。
G1 使用的是 SATB(snapshot-at-the-beginning),在初始标记(STW)时生产快照记录所有的存活对象。并发标记阶段,所有新建的对象都认为是存活的(解决新建对象漏标的问题),并且记录即将被修改引用关系的白对象的旧引用(satb_mark_queue),在清理阶段,satb_mark_queue 为根进行一遍扫描(解决修改对象漏标的问题),并且 satb_mark_queue 中的对象在下一次并发标记时会被处理。
不论是新建对象还是修改对象都有可能产生浮动垃圾。
GC 过程
JDK10 之前的 G1 GC 只有 YoungGC 和 MixedGC,FullGC 处理会交给单线程的 Serial Old 垃圾收集器。
Young GC
Young GC 主要是对 Eden 区进行垃圾回收,在 Eden 空间耗尽(达到设定的阈值)时会被触发。每次 YoungGC 会回收所有 Eden 以及 Survivor 区,并且将存活对象复制到 Survivor 区或 Old 区(Eden -> Survivor,Eden -> Old,Survivor -> Old,晋升到 Old 区依赖 PLAB 的计算结果),最终 Eden 空间的数据为空,GC 停止工作。
YoungGC 的过程如下:
扫描 GC Roots 对象,需要 Stop the world
更新 RSet
扫描 RSet,找到 Old 区对 Eden 区或者 Survivor 区的引用
扫描出的存活的对象到拷贝到 Survivor/Old 区
Mixed GC
Mix GC 会对新生代和部分老年代 Regions 进行垃圾回收,老年代的 Regions 根据算法选择加入到 CSet 中。相关控制参数:
-XX:G1HeapWastePercent=n
在一次 YoungGC 之后,可以允许的堆垃圾百占比,超过这个值就会触发 MixedGC。
-XX:G1MixedGCLiveThresholdPercent
(默认为一个 Region 分区的 85% 大小),任何一个低于阈值的老年代分区都会被包含在混合收集的 CSet 中。
-XX:G1OldCSetRegionThresholdPercent
(缺省为堆的10%),CSet 包含 Regions 的总大小,占堆的比例不超过阈值。
MixedGC 一般会发生在一次 YoungGC 后面,回收过程可以理解为 YoungGC 后进行全局的 concurrent marking(标记 Old/H 区的存活对象),大致过程如下:
初始标记(InitingMark)
标记所有的 GC Roots,会 STW,一般会复用 YoungGC 的暂停时间
根分区标记(RootRegionScan)
这个阶段 GC 的线程可以和应用线程并发运行。其主要扫描初始标记以及之前 YoungGC 对象转移到的 Survivor 分区,并标记 Survivor 区中引用的对象。所以此阶段的 Survivor 分区也叫根分区(RootRegion)
并发标记(ConcurrentMark)
并发标记阶段是并发且多线程,标记所有非空闲分区的存活对象,使用了 SATB 算法。
默认会使用 -XX:ParallelGCThreads=n
线程总数的1/4来进行并发标记,或者使用 -XX:ConcGCThreads=n
来设置。
重新标记(Remark)
主要处理并发标记阶段未标记到的存活对象(可以参考上文的 SATB 处理),这个阶段会 STW。
过多使用引用对象(弱,软,虚)会导致重新标记时间过长。
清除(Cleanup)
在清除阶段会 STW,主要工作:1. SATB 会进行缓存/指针的更新;2. 识别所有空闲分区;3. 识别出回收效率高的老年代分区;4. 更新 RSet;
清除阶段之后,会对存活对象进行转移(复制算法)到其他可用分区,所以当前的分区就变成了新的可用分区。主要是为了解决分区内的碎片问题。
Full GC
G1 在对象复制/转移失败或者没法分配足够内存(比如巨型对象没有足够的连续分区分配)时,会触发 FullGC。FullGC 使用的是单线程的 Serial Old 回收器,所以一旦触发 FullGC 则会 STW 应用线程,并且执行效率很慢。
G1 的使用场景
G1 的首要目标是为有大容量内存的系统提供一个保证 GC 低延迟的解决方案,堆内存在 6GB 及以上,保证稳定和可预测的暂停时间。
如果应用程序具有如下的一个或多个特征,那么将垃圾收集器从 CMS 或 ParallelOldGC 切换到 G1 将会大大提升性能:
Full GC 次数太频繁或者消耗时间太长.
对象分配的频率或代数提升(promotion)显著变化.
太长的垃圾回收或内存整理时间(超过0.5~1秒)
版权声明: 本文为 InfoQ 作者【Alex🐒】的原创文章。
原文链接:【http://xie.infoq.cn/article/87b7cb4629f2a4df8fffa8671】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论