JVM性能分析—— 一文带你读懂 CMS 垃圾收集器收集流程

概述

在详细讲解 CMS 与 G1 垃圾收集器之前,先介绍其中的一些知识点,方便后续理解。

跨代引用

跨代引用问题

跨代引用指的是一个老年代对象引用了一个年轻代对象。这种引用关系跨越了代的边界,称为跨代引用。

假如要现在进行一次只局限于新生代区域内的收集(Minor GC),但新生代中的对象是完全有可能被老年代所引用的,为了找出该区域中的存活对象,除了遍历 GC Roots 之外, 还要额外遍历整个老年代中所有对象来确保可达性分析结果的正确性,反过来也是一样。

  • 新生代引用老年代不算在跨代引用范围内:因为新生代收集(Minor GC)的目标仅关注新生代对象的存活状态,老年代对象的生命周期由老年代收集(Major GC)或Full GC负责,与 Minor GC 无关。
    • 仅 CMS 支持单独老年代收集:需特殊处理新生代→老年代的引用(通过增量更新处理),但这种场景非常罕见。
    • 其他收集器(如 G1、Parallel):老年代收集通常伴随新生代收集(如 Mixed GC),或需扫描全堆(如 Full GC),此时新生代→老年代的引用会被自然处理。(Major GC 或 Full GC 每小时甚至数天触发一次,全量扫描新生代的成本可接受。)

跨代引用相对于同代引用来说仅占极少数,不应再为了少量的跨代引用去扫描整个老年代。如果不加处理,在进行垃圾收集时,需要扫描整个老年代以来查找垃圾对象,这可能会导致较长的停顿时间。为了解决这些问题,分代垃圾收集器通常会采用卡表(Card Table)机制来跟踪跨代引用。把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生 Minor GC 时,只有包含了跨代引用的小块内存里的对象才会被加入到 GC Roots 进行扫描。虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值) 时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。

记忆集(Remember Set)和卡表(Card Table)

使用记忆集来缩减 GC Roots 扫描范围的问题

记忆集(Remember Set):是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。

  • 记忆集是老年代对象持有的指向年轻代对象的引用的集合。
  • 它用于记录老年代对象到年轻代对象的跨代引用关系。
  • 当年轻代发生垃圾收集时,收集器只需扫描记忆集,而不需要扫描整个老年代。这大大提高了年轻代收集的效率。

卡表(Card Table)是记忆集的一种具体实现,卡表将堆内存划分为一个个特定大小的内存块,被称作 “卡页”(Card Page),一个卡页的内存中通常包含不止一个对象, 只要卡页内有一个(或更多)对象的字段存在着跨代引用变化, 那就将对应卡页标识为1,称这个卡页变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的卡页,而非整个老年代,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入 GC Roots 中一并扫描,大幅减少扫描范围。

  • 卡表是一种辅助数据结构,用于记录老年代对象引用关系的变化。
  • 它将老年代的内存空间划分为多个固定大小的 “卡页”(Card Page)。
  • 当老年代中的对象发生引用关系的变化时,虚拟机会将相应的 “卡页” 标记为"脏"(dirty)。
  • 在年轻代垃圾收集时,收集器只需扫描那些被标记为"脏"的卡,就可以找到跨代引用,而不需要扫描整个老年代。

写屏障

解决如何维护卡表状态的问题,例如它们何时变脏、谁来把它们变脏等

变脏时间点原则上应该发生在引用类型字段赋值的那一刻。写屏障可以看作在虚拟机层面对 “引用类型字段赋值” 这个动作的AOP切面,在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作。

应用写屏障后,虚拟机就会为所有赋值操作生成相应的指令,一旦收集器在写屏障中增加了更新卡表操作,无论更新的是不是老年代对新生代对象的引用,每次只要对引用进行更新,就会产生额外的开销,不过这个开销与 Min

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值