「JVM 内存管理」HotSpot GC 算法细节实现

JVM 实现 GC 必须对算法的执行效率有保障;

1. 根节点枚举

固定的 GC Roots 节点主要在全局性的引用(常量或类静态属性)和执行上下文(栈帧中本地变量表);

由于目前主流的 JVM 都使用了准确式垃圾收集,根节点枚举并不需要一个不漏地检查所有执行上下文和全局引用,JVM 可以通过更直接的办法找到这些对象引用的存放地址(HotSpot 使用了一组 OopMap 结构实现;在类加载完成时,根据对象的内存偏移量找到指定类型的对象;在即时编译时,在特定位置记录下栈和寄存器里的普通对象引用;这样 GC 扫描时可以只针对这些地址;OOP,Ordinary Object Pointer,普通对象指针);

OopMap 解决全局收缩问题的缺点是,如果每条指令都生成对应 OopMap,将需要大量额外存储空间,这使得 GC 的空间成本变得高昂;

在可达性分析过程中,跟节点枚举必须停顿(STW)保持内存一致性,查找引用链的过程可以与用户线程并发;

2. 安全点

前文提到的特定位置(即时编译时允许普通对象引用的时间点)即是所谓安全点(safepoint);强制要求必须执行到安全点才能 STW

安全点位置的选取基本是以是否具有让程序长时间执行的特征为标准进行的;长时间执行最明显的特征就是指令序列的复用(如方法调用、循环跳转、异常跳转等);

让所有线程(执行 JNI 调用的线程除外)都跑到最近的安全点,然后 STW;

  • 抢先式中断(Preemptive Suspension),不需要线程执行的代码主动配合;在 GC 时,先让所有线程中断,然后恢复不在安全点的线程,直到它跑到安全点上再重新中断;(几乎没有 VM 实现采用抢占式中断);
  • 主动式中断(Voluntary Suspension),当 GC 需要中断时,仅仅简单地设置一个标志位(不直接对线程操作),各线程执行过程中不停轮询这个标志,一旦发现标志为真,线程就在最近的安全点主动中断;

为了使轮询标志为的操作足够高效,HotSpot 使用内存保护陷阱的方式,把轮询操作精简至只有一条汇编指令的程度;(指令包含轮询逻辑,当需要中断时,JVM 将指定内存设置为不可读,从而使指令产生一个自陷异常信号,然后通过预先注册的异常处理器实现中断挂起);

3. 安全区域

安全点无法解决不执行(线程处于 Sleep 或 Blocked 状态,VM 不可能持续等待这样的线程进入安全点)的场景;这时需要引入安全区域(Safe Region);

安全区域指在一段代码段中,对象引用关系不会发生变化,在这个区域任意点发生 GC 都是安全的;

当线程执行到安全区域,需要标记自己已进入安全区域,当 VM 发起 GC 时,可以不必管已声明自己在安全区域的线程;当线程执行到离开安全区域时,需要检查 VM 是否已完成根节点枚举,若完成则直接离开安全区域,否则需要一直等待,直到完成方可离开安全区域;

4. 记忆集与卡表

记忆集(Remembered Set),一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构(抽象是指只定义了记忆集的行为意图,类似一个接口;记忆集只用于判断非收集区是否存在指向收集区的指针);用于解决跨代引用中整区加入 GC Roots 的问题;

记忆集的记录精度

  • 字长精度,精确到一个机器字长(处理器的寻址位数,如 32/64 位),包含跨代指针;
  • 对象精度,精确到一个对象,对象中字段包含跨代指针;
  • 卡精度,精确到一块内存区域,区域内对象包含跨代指针;

卡表(Card Table),以卡精度实现记忆集的一种方式;也是最常用的实现方式;根据卡页内存是否存在跨代引用(元素变脏,Dirty),标记对应卡表元素,可以只将被标记的卡页中的对象加入 GC Roots 进行扫描;

卡页(Card Page),卡表的每一个元素对应的一块特定大小的内存块;HotSpot 的卡页是 2^9 字节;

5. 写屏障与伪共享

其他区域的对象引用了本区域对象,则对应卡表元素应该变脏;而变脏时间点是引用类型字段赋值的时刻;

写屏障可以看作是 VM 层面对引用类型字段赋值这个动作的 AOP 切面,用于解决卡表元素的维护问题;

写屏障根据覆盖的时刻可以分为写前屏障(Pre-Write Barrier,赋值前)和写后屏障(Post-Write Barrier,赋值后),G1 之前都只用了写后屏障;

伪共享(False Sharing)问题,当多线程修改相互独立的变量时,若这些变量共享同一个缓存行(Cache Line,现代中央处理器的缓存系统的存储单位),会彼此影响性能;处理并发底层细节经常要考虑的;

JDK 7 后的 HotSpot 增加了 -XX:+UseCondCardMark 参数,可以开启卡表的更新条件判断(先检查卡表标记,只有卡表元素未被标记时才会被标记变脏);

6. 并发的可达性分析

可达性分析的根节点枚举阶段涉及的 GC Roots 相对较少且固定,在如 OopMap 等优化技巧加持下,它带来的停顿是相对可以接受的;而引用链路遍历的对象是与 Java Heap 大小直接成正比的,这可能带来更长的停顿;

为何标记过程需要保障对象引用关系的一致性?

在标记时修改对象引用关系,可能把原本消亡对象错误标记为存活(可容忍),或者把存活对象错误标记为消亡(发生错误);

三色标记(Tri-color Marking

  • 白色,表示对象尚未被 GC 访问过;可达性分析开始阶段,所有对象都是白色;可达性结束时,仍是白色的对象即不可达对象;
  • 黑色,表示对象及其所有直接引用的对象都已经扫描完;它是存活的,其他对象引用它时,无需重复扫描;
  • 灰色,表示对象已被 GC 访问,但其至少有一个引用还未扫描完;

对象消失的必要条件

  • 条件 1,插入一条或多条从黑色对象到白色对象的新引用;
  • 条件 2,删除全部灰色对象到该白色对象的直接或间接引用;

实现并发扫描的方式

  • 增量更新(Incremental Update),破坏条件 1;当黑色对象插入新的指向白色对象的引用关系时,将这个新的引用关系记录下来,在并发扫描结束后,再以记录的引用关系中黑色对象为根,附加扫描一次(将该类黑色对象变回灰色,并在第一轮扫描结束后从这些灰色对象附加第二轮扫描);(CMS);
  • 原始快照(Snapshot At The Begining,SATB),破坏条件 2;当灰色对象要删除指向白色对象的引用时,将要删除的引用关系记录下来,在并发扫描结束后,以这些灰色对象为根,附加扫描一次(无论引用关系是否被删除,按照开始扫描那一刻的引用关系扫描);(G1、Shenandoah);

上一篇:「JVM 内存管理」GC 算法理论与发展过程
下一篇:「JVM 内存管理」7 款经典 GC

PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!


参考资料:

  • [1]《深入理解 Java 虚拟机》
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三余知行

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值