GC Roots,可达性分析算法,HotSpot的算法细节实现

保守式GC

和精确式GC相反,保守式GC不能准确的识别每一个无用对象,但是能保证在不会错误的回收存活的对象的情况下回收一部分无用对象。保守式GC并不需要额外的数据来支持查找对对象的引用,它将所有内存数据假定为指针,通过一些条件来判定这个指针是否是一个合法的对象的引用。

准确式GC

精确式GC是指在回收过程中能准确的识别和回收每一个无用对象的GC方式,为了准确识别每一个对象的引用,通常要求一些额外的数据,这些数据通常对用户程序是透明的。

  1. 正确的根,可以直接识别出是指针还是非指针,都需要语言处理程序加工,即为准确式GC(Exact GC)
  2. 打标签,将不明确的根的所有非指针与指针区分开。32位系统的指针是4的倍数,低2位一定是0,因此可以让非指针左移一位,而后将最后一位置1,如果溢出则换一个大的数据类型。
  3. 不把寄存器和栈等当作根,而由处理程序来创建根。
  4. 优点在于不存在指针不明确,可以使用复制移动算法。
  5. 缺点则是需要语言处理程序对GC做支持,而且打标签等方式需要消耗资源与性能。

可达性分析算法思路

基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

还有一些对象可以在一定条件下被回收,也即是Java中的四种引用类型:

强引用:对象赋予,处于可达状态,不能被回收

软引用:SoftReference, 内存不足会被回收,用于内存敏感(少)的程序中

SoftReference<String> ref = new SoftReference<String>("Hello world");
String value = ref.get();// 如果 内存被回收了,value == null

弱引用:weakReference, 只要GC一运行,就回收,不管内存够不够用。用的时候用weakReference.get(),不要把.get()重新赋值,不然就是一个强引用,垃圾回收机制就不会再回收。

WeakReference<People> peopleWeakReference = new WeakReference(new People());
System.gc();
Thread.sleep(1000);
System.out.println(peopleWeakReference.get());// 如果 内存被回收了,value == null

虚引用:PhantomReference( Phantom : 幽灵;幻影,幻觉 ) ,不能单独使用,必须和引用队列联合使用。

两次标记

  • 可达性分析算法分析发现没有与GC Roots相关联的对象会被第一次标记
  • 之后会进行一次筛选,将可以执行finalize()方法的对象放到F-Queue 队列中
  • 执行队列中的finalize()方法,但是不保证执行完成,finalize()可以实现对此对象的引用代码,使对象存活(但是不建议这么做)。
  • 第二次标记,然后进入到垃圾回收。

finalize() 究竟是做什么的呢? 它最主要的用途是回收特殊渠道申请的内存。Java 程序有垃圾回收器,所以一般情况下内存问题不用程序员操心。但有一种 JNI(Java Native Interface)调用non-Java 程序(C 或 C++), finalize() 的工作就是回收这部分的内存。

HotSpot的算法细节实现

根节点枚举,安全点,安全区域,记忆集与卡表,写屏障,并发的可达性分析(增量更新和原始快照)

枚举根节点

在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
  • 还有GC分代情况下老年代对新生代的引用,即跨代引用。

OOPMap, ordinary object pointer map

Hotspot虚拟机中是准确式GC,所以需要知道每一块内存中的数据是不是指针。

java要实现准确式GC,JVM就要能够判断出所有位置上的数据是不是指向GC堆里的引用(指针),包括活动记录(栈+寄存器)里的数据。

每个方法可能会有好几个oopMap,就是根据“安全点(safepoint)”把一个方法的代码分成几段,每一段代码一个oopMap,作用域自然也仅限于这一段代码。

在HotSpot中,对象的类型信息里有记录自己的OOPMap,记录了在该类型的对象内什么偏移量上是什么类型的数据。所以对对象的扫描可以是准确的;这些类型数据是在类加载过程中计算得到的。

https://www.cnblogs.com/strinkbug/p/6376525.html?utm_source=itdadao&utm_medium=referral

安全点

在OopMap的协助下,HotSpot可以快速准确地完成GC Roots枚举,但一个很现实的问题随之而 来:可能导致引用关系变化,或者说导致OopMap内容变化的指令非常多,如果为每一条指令都生成 对应的OopMap,那将会需要大量的额外存储空间。

实际上HotSpot也的确没有为每条指令都生成OopMap,前面已经提到,只是在“特定的位置”记录 了这些信息,这些位置被称为安全点(Safepoint)。

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

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

HotSpot中GC不是在任意位置都可以进入,而只能在safepoint处进入。 

安全区域

安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。

记忆集与卡表

所有涉及部分区域收集(Partial GC)行为的垃圾收集器,典型的如G1、ZGC和Shenandoah收集器,都会面临跨代引用的问题,

写屏障

在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的。

写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴内。

并发的可达性分析(增量更新和原始快照)

从GC Roots再继续往下遍历对象图,与Java堆的容量成正比,如果停止用户线程,只有GC线程运行来遍历对象图的话,Stop the world导致用户线程的停顿时间就会非常长。

所以解决方案是用户线程与GC收集器是并发工作,与此同时又会导致一个问题:

同时用户线程在修改引用关系——即修改对象图的结构。

并发执行:把原本存活的对象错误标记为已消亡

这样可能出现两种后果。一种是把原本消亡的对象错误标记为存活,这不是好事,但其实是可以容忍的,只不过产生了一点逃过本次收集的浮动垃圾而已,下次收集清理掉就好。另一种是把原本存活的对象错误标记为已消亡,这就是非常致命的后果了,程序肯定会因此发生错误。

同时满足两个条件就会出现 “把原本存活的对象错误标记为已消亡” 这个问题:黑色表示已扫描到,灰色表示正在扫描(或者说没扫描完),白色表示GCRoots引用不到。

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

也就是在对图的扫描过程中,用户线程不对这个对象引用,所以节点(对象)正好被扫描,被标记为垃圾,但是扫描过后用户线程又引用了这个对象已经扫描过了GC肯定不会再次扫描,所以就导致存活的对象被清理。

解决方案:增量更新和原始快照,(写屏障实现)

增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。

相当于重新再扫描一次


原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索

按原始快照来算,可能会出现垃圾被留下来的情况?

CMS是基于增量更新来做并发标记的,G1、Shenandoah则是用原始快照来实现。





除非注明,否则均为一叶呼呼原创文章,转载必须以链接形式标明本文链接

本文链接:http://www.yiyehu.tech/archives/2020/07/10/gc-roots

发表评论

电子邮件地址不会被公开。 必填项已用*标注