HotSpot 算法细节

371 阅读8分钟

我正在参加「掘金·启航计划」

1、本节总体概览

章节

HotSpot 基于可达性分析法实现垃圾收集,其中的一些算法细节有:

OopMap安全点安全区域记忆集与卡表写屏障并发的可达性分析

收集流程

可达性分析算法实现的垃圾回收机制需要经历两个步骤:根节点枚举 -> 查找引用链

各章节介绍

OopMap是记录哪些位置存在引用的一种数据结构,从而对根节点枚举进行速度优化,同时也帮助 HotSpot 实现准确式内存管理

安全点是对程序执行过程中安全记录OopMap的保障

安全区域是对程序"中断过程"中安全记录OopMap的保障

记忆集与卡表是记录跨区域引用从而对根节点枚举进行速度优化

写屏障则是安全记录记忆集与卡表的保障

并发的可达性分析是对查找引用链的并发优化

根节点枚举、查找引用链概述

可达性分析算法耗时最长的查找引用链的过程已经可以做到与用户线程一起并发

根节点枚举始终还是必须在一个能保障一致性的快照中才得以进行,因此在此过程中,必须停顿所有用户线程

  • 这里的一致性是整个枚举期间执行子系统看起来就像被冻结在某个时间点上,不会出现分析过程中,根节点集合的对象引用关系还在不断变化的情况,若这点无法满足,分析结果准确性也就无法保证

2、OopMap

2.1、诞生原因

栈中的局部变量表和对象的字段中有些是基本变量,有些是一个引用(指针),GC Roots 枚举肯定是要拿引用而不是基本变量。

但是直接去找引用,你需要对每个变量都判断一下他是不是引用,然后加入 GC Roots 中,这样相当于对这些空间的全盘扫描分析,显然很浪费时间

为避免对这些空间进行全盘扫描,使用一个数据结构记录下这些空间里引用的偏移量,找这片空间的引用时,直接去这个数据结构中拿就可以,这是典型的空间换时间的手段

从而可以快速实现 GC Root 枚举,而且也帮助实现了准确式内存管理

在 HotSpot 的解决方案中,是使用一组称为 OopMap 的数据结构来达到这个目的

2.2、结构

image.png OopMap{ebx=Oop [16]=Oop off=142}

指明了 EBX 寄存器和栈中偏移量为 16 的内存区域中各有一个普通对象指针,有效范围从该 OopMap 的位置偏移 142 为止,即 hlt 指令

2.3、优点

对于 根节点枚举 的促进:其实就是准确式 GC 的优点

将引用信息记录下来,避免收集器对这个区域所有的属性、对象等进行一个不漏的分析,可以直接从OopMap中得到这些信息,加快了根节点枚举的速度,避免长时间停顿

2.4、创建时机

OopMap 中存储了两种对象的引用

  1. 栈里和寄存器里的引用

    • 在即时编译过程中,会在特定的位置记录下栈里和寄存器里哪些位置是引用
  2. 对象内的引用

    • 类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来

这样收集器在扫描时就可以直接得到这些信息了,并不需要一个不漏地检查完所有执行上下文和全局的引用位置

3、安全点

3.1、OopMap 新问题

可能导致引用关系变化,或者说导致 OopMap 的内容变化的指令非常多,如果为每一条指令都生成对应的 OopMap,那将会需要大量的额外存储空间,这样垃圾收集伴随而来的空间成本就会变得无法忍受的昂贵

3.2、安全点(Safe Point)

程序执行过程中的特定位置设立安全点

当垃圾收集指令发出时,不会立即停止线程,而是除了执行 JNI 调用的线程,其余的用户线程都必须选择就近的安全点停顿,等待指令才能恢复执行

当程序执行到安全点,会检测是否将要发生 GC,不发生则继续执行,发生则停顿下来,等待 GC 结束后恢复线程的通知

有了安全点的设定,也就决定了用户程序执行时并非在代码指令流的任意位置都能够停下来开始垃圾收集,而是强制要求必须执行到安全点才能够暂停

3.3、安全点选取

考虑

安全点选取太少,则收集器等待用户线程执行到安全点的等待时间过长

安全点选取太多,则会过分增大运行时的内存负荷

选取标准

基本上是以是否具有让程序长时间等待的特征为标准选取的

具体标准

长时间执行的最明显特征就是指令序列的复用

例如方法调用、循环跳转、异常跳转等都属于指令序列复用,所以只有具有这些功能的指令才会产生安全点

3.4、停顿的实现方式

抢先式中断(Preemptive Suspension)

特点

  • 抢先式中断不需要线程的执行代码主动去配合
  • 现在几乎没有虚拟机实现采用抢先式中断来暂停线程响应 GC 事件

流程

  1. 当垃圾收集发生时,系统首先把所有用户线程全部中断
  2. 如果发现有用户线程中断的地方不再安全点上,就恢复这条线程执行,让它一会儿再重新中断,直到跑到安全点上

主动式中断(Voluntary Suspension)

流程

  1. 当垃圾收集需要中断线程时,不直接对线程操作,仅仅简单的设置一个标志位
  2. 各个线程执行过程中会主动去轮询这个标志
  3. 一旦发现中断标志为 真 时就自己在最近的安全点上主动中断挂起

3.5、主动式中断的轮询

轮询

由于轮询操作在代码中会频繁出现,这需要他足够高效

HotSpot 使用内存保护陷阱的方式,把轮询操作精简至只有一条汇编指令的程度

轮询流程

  1. 当需要暂停用户线程时,虚拟机会把 0x160100 的内存页设置为不可读
  2. 当线程执行到 test 指令时就会产生一个自陷异常信号
  3. 然后在预先注册的异常处理器中挂起线程实现等待

这样仅通过一条汇编指令便完成安全点轮询和触发线程中断了

轮询点的设立

  1. 轮询标志的地方和安全点是重合的

  2. 所有创建对象和其他需要在Java堆上分配内存的地方

    • 这是为了检查是否即将要发生垃圾收集,避免没有足够内存分配新对象

4、安全区域

4.1、安全点 新问题

安全点机制保证了线程执行时,在不太长的时间内就会遇到可以进入垃圾收集过程的安全点

但是程序不执行的时候是无法保证的,所谓程序不执行就是没有分配处理器时间

比如用户线程处于sleep状态或者blocked状态,这时候线程无法响应虚拟机的中断请求,不能再走到安全的地方去中断挂起自己,虚拟机也显然不可能持续等待线程重新被激活分配处理器时间

4.2、安全区域(Safe Region)

安全区域是指能够确保某一段代码中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的

我们也可以把安全区域看做被扩展拉伸了的安全点

4.3、安全区域执行过程

  1. 当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域

    • 那样当这段时间里虚拟机要发起垃圾收集时,就不必去管这些已声明自己在安全区域内的线程了
  2. 当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的阶段)

  3. 如果完成了,那线程就当做没事发生过,继续执行

  4. 否则他就必须一直等待,直到收到可以离开安全区域的信号为止

未完待续……

参考文献

《深入理解 Java 虚拟机》第三版