《深入理解Java虚拟机》ch3.4 HotSpot的算法细节实现

HotSpot的算法细节实现

​ HotSpot虚拟机通过 根节点枚举算法 判断需要回收的对象;运用 安全点安全区域 解决了多线程查找根节点的问题;其中跨代引用使用 记忆集 中的 卡表 进行维护,而卡表的维护由 写屏障 解决;采用 增量更新 或者 原始快照 方法解决了并发中可达性分析算法遇到的问题。

根节点枚举算法

  • 必须暂停用户线程 (STW,stop the world)

    根节点的枚举必须在保证一致性的快照中执行 —— 枚举期间根节点集合的对象引用关系保持不变。

  • 采用 OopMap (普通对象指针map) 来快速获取 GC Roots

    在HotSpot中,对象的类型信息里有记录自己的 OopMap,记录了在该类型的对象内什么偏移量上是什么类型的数据。

    OopMap 在不被使用时会被 JVM 压缩,只有在 GC 时才会按需解压出来。

    • OopMap 的前提是虚拟机实现的是 准确式GC,有以下几种GC:

      1. 保守式GC:不知道内存中数据的准确类型,在GC时从已知位置开始扫描,使用上下边界检查、对齐检查等方法来判断对象是否可以作为 GC Roots。

        保守式GC的两个问题:

        1. 由于不能准确判断类型,所以某些对象即使已经不可达但是保守式GC任然不会将其回收,这不会造成程序错误,但是会造成内存的浪费
        2. 在处理引用类型时,需要中间层 (句柄) 来支持对象的移动。如果JVM需要支持反射功能的话,保守式GC是不可取的。
      2. 半保守式GC:将类的信息存放在对象上,因此又被称为根上保守,支持部分对象的移动。

      3. 准确式GC:在外部存储下类的信息,放在映射表中,虚拟机能够直到内存中数据的准确类型,支持对象的移动。在HotSpot中被称为 OopMap。

    • 类加载动作完成的时候,就可以将对象信息 (栈和寄存器中哪些位置是引用) 加入到 OopMap 中。收集器在扫描时直接访问 OopMap 从而获得 GC Roots,不需要遍历整个栈、方法区等 (这里面有很多对象不是引用)。

    • 调用 OopMap 的方法:

      1. 解释式:每次遍历 OopMap —— HotSpot 虚拟机采用解释式方法来访问 OopMap。
      2. 编译式:为 OopMap 生成单独的访问代码,访问时直接调用代码。
    • JNI 方法没有 OopMap。在调用 JNI 方法时,JVM 为其包装了一层句柄,每次访问都有句柄的拆箱、装箱过程,因此 JNI 方法效率较低。

安全点

在安全点对 OopMap 进行维护,即在安全点处才能发起 GC。

  • 更新 OopMap 的问题:

    • 很多指令可以改变引用关系,如果在每一条指令上操作 OopMap 的话,效率会非常低下。因此HotSpot选择在一些特定位置停下来更新OopMap:

      1. 循环的末尾
      2. 方法临返回前
      3. 调用方法的call指令后
      4. 可能抛异常的位置

      因此一个方法会有多个安全点,对应的也有多个 OopMap 去对应不同区域的内存情况。

  • 如果采用安全点,需要确保多线程并发时所有线程都处于安全点

    • 抢先式中断 (Premmptive Suspension):将所有线程中断,如果有线程不在其安全点,那么恢复此现成的运行直到最近的一个安全点。
    • 主动式终端 (Voluntary Suspension):设置一个所有线程共有的标志位,线程运行时不断地检查标志位,当设置标志位为真时所有线程主动地在最近的安全点挂起。
      • 由于要不断地访问这个标志位,所以轮询操作精简为一条汇编指令以提高性能 —— 当需要暂停线程时,将作为标志位的内存页设置为不可读,线程抛出异常后经由异常处理器处理。

安全区域

安全区域是安全点的一个超集。在安全区域内,引用关系不会发生变化。

  • 安全区域的意义:多线程并发时,并非有所有的线程都正在执行,有些线程会处于 Sleep 或者 Blocked 状态,从而无法响应虚拟机的中断请求在安全点挂起。
  • 安全区域的使用方法:
    • 线程执行到安全区域时,标识自己进入了安全区域,JVM 发起 GC 时就不会对这些线程进行操作。
    • 线程离开安全区域时,如果 JVM 还未完成 GC,那么等待 JVM 完成 GC。

记忆集与卡表

记忆集:用于记录从非收集区域指向收集区域的指针集合的抽象数据结构

  • 记忆集的访问精度:字长精度、对象精度、卡精度。

卡表:实现记忆集的数据结构,HotSpot 中使用字节数组作为卡表。

CARD_TABLE [this address >> 9] = 1;
  • HotSpot 虚拟机的卡表中内存区域大小 (卡页) 是 512 字节 (29),只要这个内存区域内有跨代引用,那么这个卡页被标志为1。

写屏障

在发生赋值操作时就可能需要对卡表进行维护,为了同时支持字节码指令和机器码指令,维护卡表的动作下降到机器码层面放到了每个赋值操作中。

void opp_field_store(opp* field, oop new_value) {
    *field = new_value; // 引用字段赋值操作
    post_write_barrier(field, new_value); // 写后屏障,在这里实现对卡表状态的更新
}
  • 写屏障可以看作为虚拟机层面对“引用字段类型赋值” 这个动作的 AOP 切面 (我个人感觉和 c++ 的运算符重载差不多?等以后学到了再来更新吧)
  • 写屏障会带来一定的开销,不过仍然优于扫描整个老年代。由于CPU缓存中缓存行的存在,高并发会卡表会遇到伪共享问题,如果卡表某个卡页已经为 true,那不就不需要更新了,减少一次更新操作。
if(CARD_TABLE [this address >> 9] != 1)
    CARD_TABLE [this address >> 9] = 1;

并发的可达性分析

和根节点的枚举算法一样,可达性分析原则上也需要维护一致性。

不过由于对象实在是太多了,如果中断用户线程进行可达性分析是无法忍受的。

  • 三色标记法 (Tri-color Marking) (依我看,和 tarjan 求最大连通分量的思路很相似)

    • 白色:尚未被垃圾收集器访问。开始前大家都是白色,结束后认为白色的就是不可达的对象。
    • 黑色:自己以及这个对象的所有引用都被垃圾收集器访问过了。
    • 灰色:被垃圾收集器访问过,但是还有一个或者多个引用未被垃圾收集访问。
  • 并发条件下进行三色标记时,会存在两种典型的问题:

    1. 浮动垃圾(多标):将应该被清除的对象,误标记为存活对象。会造成浮动垃圾,但是问题不大,可以在下个GC时被回收;

    2. 对象消失(漏标):将应该存活的对象,误标记为需要清理的对象,会造成程序运行错误!

      只有在同时满足以下两个条件时,才会发生这个错误:

      • 赋值器插入了一条或者多条从黑色对象到白色对象的新引用
      • 赋值器删除了所有灰色对象到该白色对象的直接或间接引用
  • 处理第二个问题的方法:使其中一个条件不被满足即可

    • 增量更新 (Incremental Update) + 写屏障:当黑色对象插入新的指向白色对象的引用时,将该黑色对象记录下来,最后再以这些黑色对象为根节点再进行一次 GC 即可。(CMS垃圾收集器)
    void write_barrier(obj, field, newobj) {
        // 如果建立引用的出发对象是黑色,且指向对象是白色或者灰色
        // 那么将出发对象涂成灰色
        if($gc_phase == GC_MARK && obj.mark == TRUE && newobj.mark == FALSE) {
            obj.mark = FALSE;
            push(obj, $mark_stack);
        }
        *field = newobj;
    }
    
    • 原始快照 (Snapshot At The Begining, STAB) + 写屏障:当灰色对象要删除指向白色对象的引用关系时,将这个记录下来,并发标记完成后,对该记录进行重新扫描。(G1垃圾收集器)

      这意味着不能够准确地清除对象,实际上不会删除引用,在后续的深度 GC 后才能回收

      void write_barrier(oldobj, field, newobj) {
          oldobj = *field; // 记录下旧的引用
          // 如果标记前是白色对象,那么将其置为灰色对象
          if(gc_phase == GC_MARK && oldobj.mark == FALSE) {
              oldobj.mark = TRUE;
              push(oldobj, $mark_stack);
          }
          *field = newobj;
      }
      

    两种方法的优劣分析:

    • 增量分析比较准确,原始快照会造成浮动垃圾
    • 增量分析需要重新枚举 GC Roots,而原始快照只需要枚举标记的灰色节点
posted @   PigPigHero  阅读(129)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示