前言
阅读《深入理解Java虚拟机》相关笔记。
根节点枚举
固定可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)。
迄今为止,所有收集器在根节点枚举这一步骤都是必须暂停用户线程的。
目前主流的Java虚拟机使用的都是准确式垃圾收集(虚拟机可以知道内存中某个位置的数据具体是什么类型),当用户线程停顿下来时,其实并不需要一个不漏的检查完所有执行上下文和全局的引用位置,虚拟机是有办法直接得到哪些地方存放着对象引用的。
HotSpot使用一组称为OopMap的数据结构记录在类加载动作完成的时候计算出来的对象什么偏移量上是什么类型的数据。在即时编译的过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。
安全点
在OopMap帮助下,HotSpot可以快速准确地完成GC Roots枚举,问题:可能导致引用关系变化,或者说导致OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,将会需要大量的额外存储空间。
HotSpot在特定的位置–安全点记录这些信息。有了安全点的设计,强制要求必须执行到达安全点后才能暂停。
安全点选择的标准:”是否具有让程序长时间执行的特征“。
如何在垃圾收集发生时让所有线程跑到最近的安全点:
- 抢断式中断:先中断所有用户线程,如不在安全点上则继续执行到安全点停止。
- 主动式中断:设置标志位,轮询标志,一旦发现标志为真就在最近的安全点主动
中断挂起。轮询标志和安全点是重合的,另外还要加上所有创建对象和其他需要在Java堆上分配内存的地方,这是为了检查是否即将发生垃圾收集,避免没有足够的内存分配新对象。
HotSpot使用内存保护陷阱的方式将轮询操作简化至一条汇编指令。
安全区域
安全点机制保证了程序执行时,在不长的时间内就会遇到可进入垃圾收过程的安全点。但是程序”不执行“比如Sleep状态或者Blocked状态。引入安全区域(Safe Region)。
安全区域是确保在某一段代码片段中,引用关系不会发生变化。当用户线程执行到安全区域时,标识自己进入了安全区域,发起垃圾收集时就不必管这些处于安全区域的线程了。
记忆集与卡表
为解决跨代引用问题,垃圾收集器在新生代中建立了名为记忆集(Remembered Set)的数据结构。记忆集是一种用于记录从非收集区域指向手机区域的指针集合的抽象数据结构。
收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向手机区域的指针就行。可以选择粗记录粒度节省存储和维护成本:
- 字长精度:每个记录精确到一个机器字长,该字包含跨代指针
- 对象精度:每个记录精确到每个对象,该对象里有字段含有跨代指针。
- 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。
目前最常用的记忆集实现是卡表(Card Table,卡精度)。卡表最简单的形式可以只是一个字节数组(HotSpot虚拟机)。
CARD_TABLE的每一个元素都对应着其表示的内存区域中一块特定大小的内存块,这个内存块称为“卡页”,通常2的N次幂。一个卡页内存中通常包含不止一个对象,只要卡页内有一或多个对象的字段存在跨代指针,就将对应卡表的数组元素的值标识为1,称为变脏(Dirty)。然后就可以加入GC Roots一并扫描。
写屏障
解决卡表元素维护问题,比如何时变脏,如何变脏。
当有其他区域的对象引用了本卡表区域对象时,其对应的卡表元素就应该变脏。变脏时间理论应该在发生引用类型字段赋值的那一刻。
如果是解释执行的字节码比较容易处理,如果时编译后执行,纯粹的机器指令流就比较困难。
HotSpot虚拟机里通过写屏障(Write Barrier)计数维护卡表状态。写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面(容易联想到中间件以及动态代理),在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的操作。赋值前为写前屏障,赋值后为写后屏障。
卡表在高并发情景下面临“伪共享”问题。现代中央处理器的缓存系统中是以缓存行(Cache Line)为单位存储,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响而导致性能降低。一个简单的解决方案是有条件的写屏障,只有未被标记的才将其标记为脏。因为一般会有临近访问情况,可以避免重复标记。
并发的可达性分析
想要解决或降低用户线程的停顿,就必须在一个能保障一致性的快照上进行对象图的遍历。
三色标记:
- 白色:未被垃圾收集器访问过的对象。
- 黑色:表示对象已被访问,且这个对象的所有引用都已经扫描过
- 灰色:表示对象已被扫描,但还有引用没被扫描
并发收集时,一是可能把原本消亡的对象标记为存活,不好但可以容忍到下次再收集;二是把原本存活的对象误标记为已消亡,非常致命(“对象消失”)。
对象消失(本该黑色误标白色)当且仅当同时出现(黑色不会重复扫描):
- 赋值器插入了一条或多条从黑色对象到白色对象的新引用;
- 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。
解决方案(分别破坏条件1、2):增量更新(Increasemental Update)、原始快照(Snapshot At The Beginning,SATB)
增量更新:黑色对象一旦插入了指向白色对象的引用后,记录下来,等并发扫描结束后,再以这些记录中的黑色节点为根重新扫描。黑色变灰色。
原始快照:当灰色对象要删除到白色对象引用时,记录引用,并发扫描结束后,重新扫描这些灰色节点。灰色不变。
引用关系记录和虚拟机记录操作都是通过写屏障实现。CMS基于增量更新做并发标记,G1、Shenandoah则是用原始快照实现。