通过 GC ROOT TRACING 可到达的对象肯定是活着的。但是如果遇到在以下场景,老年代进行 GC 时,如何确保图中 Current Obj 被标记为活着的?


通过扫描新生代。所以 CMS 虽然是老年代的 GC,但仍要扫描新生代。CMS GC 日志中可以看到扫描日志:

[GC[YG occupancy: 270973 K (1145600 K)]
[Rescan (parallel) , 0.2171280 secs]
[weak refs processing, 0.0000700 secs]
[scrub string table, 0.0016420 secs] [1 CMS-remark: 1996330K(2124800K)] 2267303K(3270400K), 0.2190720 secs] [Times: user=2.13 sys=0.00, real=0.22 secs]
Total time for which application threads were stopped: 0.2232820 seconds
Rescan 会扫描新生代和老年代中的对象, parallel 说明此阶段是并行进行的,并且只需要扫描新生代和老年代中活着的对象就可以了。
扫描新生代机制
Minor GC 后新生代的对象全是活着的,并且活着的对象应该很少。所以可以在扫描新生代前进行一次 Minor GC(老年代 GC 前)。CMS 提供参数:
+XX:CMSScheduleRemarkEdenSizeThreshold=2M
# 当 Eden 区占用超过 2M 时,执行可中断的并发预清理(CMS-concurrent-abortable-preclean)
+XX:CMSScheduleRemarkEdenPenetration=50
# 直到 Eden 空间使用率达到 50% 时中断,然后进入重新标记(CMS-Remark)阶段
-XX:CMSMaxAbortablePrecleanTime=5000
# 并发预清理如果超过 5000ms 未完成,直接进入重新标记阶段
如果在可中断的并发预清理阶段发生了一次 Minor GC 就会提前中断此阶段(此阶段在重新标记前,Minor GC 后新生代只剩下少量的活着的对象,就达到了优化效果),但是这个阶段不一定会发生 Minor GC(JVM 自动调度无法控制),也就是最长可能执行5s后进入重新标记阶段。
即然并发预清理阶段不能保证执行一次 Minor GC,CMS 提供另一个参数 +XX:+CMSScavengeBeforeRemark,使 CMS 重新标记阶段前强制进行一次 Minor GC。
好的一面是减少了 CMS-Remark 阶段的 STW 时间,坏的一面是 Minor GC 后紧跟着一个 CMS-Remark,增加整体的 STW 时间。
[CMS-concurrent-abortable-preclean-start]
[GC 7688.465: [ParNew: 1040940K->1464K(1044544K), 0.0165840 secs] 1343593K->304365K(2093120K), 0.0167509 secs]
[CMS-concurrent-abortable-preclean: 1.012/1.907 secs]
[GC[YG occupancy: 522484 K (1044544 K)]

看似不一定能够改善总体的 STW 情况,实际使用时需要根据测试结果调整。

扫描老年代机制

使用了 Card Table(Byte 数组),将老年代的空间分成大小为 512bytes 的块(Card),Card Table 的每一位对应老年代空间中的一个 Card。

并发标记时(此时应用线程继续运行),如果某个对象的引用发生了变化,Card Table 就标记该对象所在的 Card 为 Dirty card。

并发预清理阶段就对于老年代的重新扫描只需要扫描 Dirty card 所在的内存空间,找到所有可能活着的对象。

并发标记时对象的状态:


随后 current obj 的引用发生了变化:


current obj 所在的 card 被标记为了dirty card

随后到了 pre-cleaning 阶段,该阶段的任务之一就是标记这些在并发标记阶段被修改了的对象,之后那些通过 current obj 变得可达的对象也被标记了,同时 Dirty card 标志也被清除:


Card Table 的其他作用

上面的场景是老年代对象被新生代对象引用,另外一个场景刚好相反:新生代对象被老年代对象引用。

当有老年代引用新生代,对应的 Card Table 被标识为 Dirty card(Card Table 中是一个 byte,有八位,约定好每一位的含义就可区分哪个是引用新生代,哪个是并发标记阶段修改过的),Minor GC 通过扫描 Dirty cards 就可以很快的识别老年代引用新生代。

Tags:JVM