JVM拾遗 基于分代理论的初始标记:根结点枚举(STW)

什么是根结点枚举

  根节点枚举必须在一个能保证一致性快照的时间进行----“一致性”就比如整个系统暂停,不会出如今分析过程当中,整个饮用链还在不断变化的状况,若是不正保证的话,那么分析结果的准确性也没法保证。CMS,G1,ZGC等收集器也会发生STW。c++

  其实,在用户线程停顿下来以后,其实并不须要扫描整个堆,虚拟机有办法直接获得对象引用信息。HotSpot使用OopMap这样的数据结构来解决。数组

  在类加载完成的时候,虚拟机就会把对象内什么偏移量是什么类型信息计算出来。安全

  那么在JVM运行过程当中是如何处理的呢:安全点微信

安全点

  致使OopMap变化的因素有不少,虚拟机不可能为全部的操做都生成对应的OopMap,操做代价和空间成本过于高昂。在特定的位置记录了这些信息,这些位置称为安全点。markdown

安全点须要考虑的问题:数据结构

  1. 不能太少让GC收集器等待时间过长,不能太频繁增大内存负荷oop

    因此选取的根据是能让线程长时间执行,如方法调用,循环等post

  2. 如何让线程跑到最近的安全点:抢占式中断和主动式中断spa

    抢占式:先把全部用户线程中断,若是不在安全点,则恢复执行到安全点线程

    主动式:设置一个标识位,让线程执行时不断去轮询这个标志位

汇编级别如何实现的:HotSpot使用内存保护陷阱的方式把轮询操做精简到只有一个汇编指令(test)的程度。在执行到test指令时,在预先注册的异常处理器中挂起线程实现等待。

  那些已经sleep和block的线程如何处理:安全区域

安全区域

  安全区域指在某一个代码片断中,引用关系不会发生变化。

  线程会标识本身进入了安全区域,当线程要离开安全区域时,要检查虚拟机是否完成了根节点枚举,完成则继续执行线程,未完成继续等待,直到收到能够离开的信号。

  若是新生代中含有老年代对象的指针,不可能把老年代做为GC Roots扫描范围,那么该如何解决:记忆集与卡表

记忆集与卡表

  为解决对象跨代引用所带来的问题,垃圾收集器在新生代中创建了名为记忆集(Remembered Set)的数据结构,用以免把整个老年代加进GC Roots扫描范围。

  HotSpot对记忆集的实现即为卡表。卡表选择较为粗犷的精度来节省成本,它的每个记录对应一块内存区域,该区域内有跨代指针

  卡表最简单的形式能够只是一个字节数组,字节数组的每个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称做“卡页”(Card Page)。

  一个卡页的内存中一般包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。

  解决了如何使用记忆集来缩减GC Roots扫描范围的问题,但尚未解决卡表元素如何维护的问题,例如它们什么时候变脏、谁来把它们变脏等

写屏障

  有其余分代区域的对象引用本区域的对象时,其对应的卡表元素就应该变脏,在时间点上也应是:在本区域对象的引用被改变时。解释执行的字节码好理解,虚拟机负责每条字节码指令的执行,能够交给虚拟机负责;编译执行的场景,通过即时编译后的代码已是纯粹的机器指令流了,必须找到一个在机器码层面的手段,把维护卡表的动做放到每个赋值操做之中。

  写屏障能够看做在虚拟机层面对“引用类型字段赋值”这个动做的AOP切面

void oop_field_store (oop* field, oop new_value) {
  // 引用字段赋值操做
  *field = new_value;
  // 写后屏障,在这里完成卡表状态更新
  post_write_barrier(field, new_value);
}
复制代码

参考资料

周志明-《深刻理解Java虚拟机:JVM高级特性与最佳实践》第三版

本文由 发给官兵 创做,采用 CC BY 3.0 CN协议 进行许可。 可自由转载、引用,但需署名做者且注明文章出 处。如转载至微信公众号,请在文末添加做者公众号二维码。

相关文章
相关标签/搜索