在可达性分析中存在的问题

  1. 我们知道在可达性分析中需要从GC ROOTS出发,遍历整个对象图找出垃圾并进行回收。但是比如说进行Minor GC的时候,你要从哪些地方寻找GC ROOTS?这个的范围并不是单单只扫描整个新生代就行了。因为跨代引用的存在,你还需要扫描老年代中指向新生代的GC ROOTS 。这就又会带来一个问题,老年代中的东西是十分多的,如果我们每一次Minor GC都要扫描整个老年代,那么时间开销会是很大的。所以为了减少这部分的时间,JVM采用了空间换取时间的办法。因为我们只需要知道某一非收集区域是否有指向收集区域的指针而不需要知道所有的细节。因此在全局维护了一个记忆集,扩大了分类的粒度,对于某一块区域有指向收集区域的指针就直接标记。最后进行GC的时候只需要从记忆集中扫描对应的非收集区域即可,就不用扫描整个老年代了,节省了时间。
  2. 我们现在知道了要在哪些区域进行GC ROOTS的扫描,但是问题又出现了。对应一个运行中的程序运行着多个线程,每一个线程都有自己的运行空间,没进入一个方法就会生成一个栈帧,一个方法中的局部基本变量和引用类型都会保存在栈帧中。那么也就意味着在庞大的栈中查找GC ROOTS的工作量是极大的。所以为了快速得到哪些地方存在着GC ROOTS,JVM维护了一个OopMap来直接得到哪些地方存在对应引用,快速得到GC ROOTS。在类加载的过程中,JVM会把一个对象内什么偏移量存在着什么对象计算出来。通过OopMap来快速完成根节点的枚举。
  3. OopMap并不是一成不变的,它甚至可能每一条运行的语句都会造成OopMap的变化。如果为每一条语句都生成对应的OopMap,那么消耗太大(不是很懂为什么不能共用同一个),因此JVM设立了安全点的概念,只允许在安全点的时候进行垃圾回收。这就涉及到两个问题,线程到达安全点的方式,第一种是抢占式中断,第二种是主动式中断。当然,为了照顾目前处于阻塞和睡眠的线程,jvm还设立了安全区域的概念。
  4. 以上三点已经解决了如何高速的找到所有的GC ROOTS,那么对于可达性分析来说,还差最后一步,就是遍历整个对象图,找到垃圾。这个步骤的持续时间是很长的,所以如果在执行遍历的时候停止用户线程是非常不合理的,会造成巨大的延迟。因此,这步骤一般是会和用户线程并发执行的。但是这就又会导致一个问题,用户代码在执行的时候,对象的引用关系是在变化的,我们需要处理在并发扫描时因为用户线程的执行所带来的问题。主要问题有两个,垃圾被标记为非垃圾(问题不大,最多是产生浮动垃圾),非垃圾被标记为垃圾(问题很大,程序执行会产生问题)。为了描述解决上面的问题,引入了三色法。、
    • 白色:表示对象尚未被垃圾收集器访问过
    • 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过
    • 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过

当且仅当以下两个条件成立的时候,会出现误判

  • 赋值器插入了一条或多条从黑色对象到白色对象的新引用;
  • 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

有了上述的理论,就有了两种解决的办法,分别被应用在CMS 和 G1垃圾收集器上。

第一种是增量更新,破坏第一个条件,在并发扫描的过程中,记录所有从黑色对象到白色对象的引用。等扫描结束后,再从这些记录的黑色节点再扫描一遍

第二种是原始快照,破坏的是第二个条件,在并发扫描的过程中,记录所有的删除记录,在当前扫描的时候按最开始的视图扫描,扫描结束之后,在将记录中的灰色对象为根扫描一遍。

注:文章内容根据<<深入理解Java虚拟机>>的部分内容加上自己的理解写成,如果有错误望指正

你可能感兴趣的:(java)