前言

跨代引用

场景:

  • 年轻代的对象持有着老年代对象的引用、老年代的对象持有着年轻代对象的引用

特点:

  • 互相引用的两个对象几乎总是同生共死:
  • 如果某个新生代对象存在跨代引用,由于老年代对象难以消亡,该引用会使得新生对象在minor gc时得以存活,该对象经历多次minor gc后晋升到老年代,此时跨代引用自然也随着该对象的晋升而消失了。
  • 因此,存在跨代引用的对象较少。

跨代引用带来的问题:

  • 年轻代对象引用老年代对象:minor gc时需要扫描老年代。
  • 老年代对象引用年轻代对象:major gc时需要扫描年轻代。

解决方案:

  • 通过降低扫描对象的范围或数量来降低gc的耗时:
  • 年轻代对象引用老年代对象:借助 卡表/记忆集合 来减小minor gc时扫描老年代的范围,进而降低minor gc的时间。
  • 老年代对象引用年轻代对象:
  • CMS收集器:借助 提前触发minor gc 来减少年轻代中对象的数量,进而降低major gc的时间。
  • G1收集器:借助 记忆集合 来减少老年代gc时扫描年轻代的范围,进而降低minor gc的时间。

卡表

数据结构:

  • 卡表:jvm将老年代划分为若干个大小为512字节的区域(card),并使用一个 字节(byte)数组 来标记老年代中这些区域(card)中的对象是否持有新生代对象的引用。jvm将这个 字节数组 称为卡表(card table)。
  • 卡表中的元素:表示老年代中某块区域(card)中的对象是否持有新生代对象的引用。
  • 卡表属于points-out(我引用了谁的对象)的结构。

说明:之所以使用byte数组而不是bit数组主要是速度上的考量,现代计算机硬件都是最小按字节寻址的,没有直接存储一个bit指令,所以要用bit的话就不得不多消耗几条shift+mask指令。

原理:

  • 当老年代中的某个对象持有了新生代对象的引用时,jvm将卡表中表示该对象所在区域(card)的元素设为1,表示该对象所在区域(card)是一个 dirty card。(注意:新生代对象引用老年代对象时,老年代对象所在的区域(card)不会被标记为dirty card)。
  • 年轻代gc时只扫描dirty card中的对象,而无需扫描整个老年代中的对象,从而减少年轻代gc的停顿时间。
  • 当完成所有脏卡的扫描之后,jvm便会将所有脏卡的标识位清零。

card标记为dirty card的原理

写屏障:

  • 写屏障是一小段将card标记为dirty card的代码:检查对象的引用变更时是否出现了跨代引用(g1是跨region引用),如果出现,这将对应的card标记为dirty card。

Hotspot VM的字节码解释器和JIT编译器使用写屏障维护卡表:

  • 解释器每次执行更新引用的字节码时,都会执行一段写屏障。
  • JIT编译器在生成更新引用的代码后,也会生成一段写屏障。
  • 虽然写屏障使得应用线程增加了一些性能开销,但是minor gc的效率提高了很多,进而提高了系统的吞吐量。

思考:

minor gc时只扫描dirty card中的对象,并行收集模式下gc线程有多个,那么针对多个dirty card,这些gc线程是如何分工的呢?

步块

数据结构:

  • jvm将一定数量(默认是256)的card组成一个更大区域,这个区域称为步块(stride chunk)。

作用:

  • 多个gc线程并行收集时,每个线程每次负责扫描一个步块,其中包括:扫描该步块对应的 部分卡表 以及 该步块内dirty card中的对象。

参数:

-XX:+UnlockDiagnosticVMOptions

  • 解锁诊断虚拟机参数,否则 XX:ParGCCardsPerStrideChunk 不生效

-XX:ParGCCardsPerStrideChunk

  • 设置一个gc线程每次扫描的card数量,默认是256。
  • 不能设置的太小:太小会造成老年代stride chunk数量太多,导致gc线程在stride chunk之间切换的开销增加,进而导致gc暂停时间增加。
  • 经验值:堆大小为4G时,该值设为2k,具体取值以最终调优结果为准。参考:

记忆集合

数据结构

  • 每个region都维护着一个记忆集合(Remembered Set / Rset),收集器在标记跨代引用的对象时只需扫描(CSet中region维护的)RSet即可。
  • RSet的整体结构是一个哈希表,底层是在卡表的基础上实现的。
  • key:key记录了引用本region中对象的对象所在region的位置。
  • value:是一个集合,其元素是:其它region(由key确定是哪个region)中的对象引用本region中对象的引用及引用所在的卡表位置。
  • Rset属于points-into结构(谁引用了我的对象)

RSet、Card和Region的关系

  • 每个region被分成了多个card。
  • 不同region中的card会相互引用。
  • 图示:

java 如何跨模块引用 jvm跨代引用_老年代

  • Region1中的Card中的对象引用了Region2中的Card中的对象,蓝色实线表示的就是points-out的关系,而在Region2的RSet中,记录了Region1的Card,即红色虚线表示的关系,这就是points-into。
  • 维系RSet中的引用关系靠post-write barrier和Concurrent refinement threads来维护 。

参考:

从实际案例聊聊Java应用的GC优化 - 美团技术团队

Java Hotspot G1 GC的一些关键技术 - 美团技术团队