深入理解jvm虚拟机-Hotspot虚拟机垃圾回收算法细节实现
GC流程
一般虚拟机要开始GC的话需要进行GC Root的寻找和扫描,在找到GC Root之后进行遍历,进行可达性分析,标记然后清除
GC Root枚举实现
基于GC Root虚拟机要进行垃圾回收,但垃圾回收的第一个拦路虎出现了,如何快速找到GC Root?虚拟机它在开始垃圾回收的时候面对的是一块内存地址,如何从内存地址中快速找到GC Root的起始,这是我们要解决的议题,针对这个问题,虚拟机采用了一个OopMap的数据结构去记录GC Root的起始位置,这样虚拟机利用这个数据结构就可以快速找到GC Root并进行遍历
跨代引用?
在垃圾回收中,分Minor GC和Full GC,也就是新生代GC和老年代GC,如果程序中存在着老年代中的对象引用了新生代对象的情况,那么在Minor GC的过程中为了寻找GC Root就会去扫描整个老年代的内存区域,这样代价是巨大的,为了解决这种跨代引用的问题,虚拟机同样采用了一个叫记忆集的数据结构去记录老年代中的哪块内存存在着跨代引用的情况,那么即使在出现跨代引用的情况之后,Minor GC也不需要去扫描整个老年代的内存区域,记忆集的实现在不同的虚拟机中有不同的实现方法,在Hotspot虚拟机中采用的是卡表这种数据结构
卡表
卡表是HotSpot虚拟机对于记忆集的一种实现方式,数据结构如下图所示:
卡表中的每一个元素都会指向一块内存,这块内存的大小是512字节,如果这块内存在跨代引用的情况,则赋值为1,下列代码是对于卡表的一个赋值操作:
1 | CARD_TABLE [this address >> 9] = 1; |
如何更新卡表?
对于更新卡表的操作虚拟机采用的aop切面编程思想,在对引用类型的指针进行赋值的前后对卡表进行操作,赋值前操作时写前操作,赋值后是写后操作:
1 | void oop_field_store(oop* field, oop new_value) { |
除了写屏障的开销外,卡表在高并发场景下还面临“伪共享”[[什么是伪共享.md id=d2ab4210-72ec-11ec-9e6f-19183699c9f1]] 的状态,现代CPU的缓存系统中是以缓冲行为单位存储的,多线程修改互相独立的变量时候,如果这些变量恰好同时共享一个缓存行,就会彼此影响,导致性能降低,为解决这个问题,简单的解决方案就是在写入卡表的时候,先看卡表的标记是不是为0,如果没有被标记过才往进写。
遍历GC Root时保证一致性
安全点
在垃圾回收开始时,程序需要保证在垃圾回收的过程中保持一个引用不变的情况,为了保持这个一致性虚拟机引用了安全点的概念,实际上虚拟机并不会为每条指令都会保存一个OopMap,为了避免性能的损耗,只有在特定的位置记录OopMap,这些位置就是安全点,在真正开始垃圾回收前需要保证所有线程都跑到最近的安全点上,有两种方案:
- 抢占式中断:抢占式中断不需要线程的执行代码主动去配合,在垃圾回收发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方,不在安全点上,就恢复这条线程执行,让他一会儿再继续中断,直到跑到安全点上,目前几乎所有虚拟机都没有采用这种方式。
- 主动式中断:主动式中断的思想是当垃圾回收需要中断线程的时候,不直接对线程进行操作,仅仅简单设置一个标志位。各个线程执行过程当中,会不断主动去轮询这个标志位,一旦发现中断标志为真的时候,就自己在最近的安全点上主动中断挂起,轮巡标志保证和安全点是重合的。
安全区域
安全区域可以看作是对安全点的扩大,主要是为了适配当线程没有获取CPU资源的时候,线程处于Sleep状态或block状态,这时线程无法响应虚拟机的中断请求,无法走到安全点的位置,去挂起自己,此时,便有了安全区域的概念。安全区域是能够确保在某一段代码之中引用关系不会发生改变,因此在这个安全区域内什么时候去进行垃圾回收都是安全的,当现成要离开安全区域的时候,他会检查虚拟机是否已经完成了根节点的枚举(或者垃圾回收过程当中,需要其他暂停用户线程的阶段)
可达性分析
在可达性分析中,使用三色标记算法来推导为什么可达性分析需要在一个保持一致性的情况进行:
- 白色:表示对象没有被垃圾回收器访问过
- 黑色:对象已经被垃圾回收器访问过,且它的引用全部被垃圾回收器访问过
- 灰色:对象已经被垃圾回收器访问过,它的引用至少有一个没有被访问过
上图就表示了,假设垃圾回收在遍历至灰色结点时,由于和它相连的白色那个节点还未被访问,此时指向消失,同时已经遍历完的前一个节点又和它进行连接,那么此时本来这个对象有引用,现在变成了白色,导致别回收,这在垃圾回收是一种灾难级别的错误,出现这种错误的原因有以下几点:
- 赋值器插入了一条或者多条从黑色到白色的新引用
- 赋值器删除了全部从灰色到白色的直接或者间接引用
为了避免以上两种情况,分别产生了两种解决方案:
- 增量更新
- 原始快照
增量更新
增量更新要破坏的是第一个条件,黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根重新扫描一次,可以简单理解为黑色对象,一旦新插入了指向白色对象的引用之后,就会变成灰色对象。
原始快照
原始快照要破坏的是第二个条件。当灰色对象要删除,指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次,可以简单理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照进行扫描。