本文已被Github收录,推荐阅读与奋斗。——SocratesHotSpot采用可达性分析算法,需要根节点枚举。要找到高效的根节点枚举过程并不容易。现在Java应用程序越来越大。光是方法区的大小就动辄上百G,里面的类、常量等就是恒河沙的数量(修辞手法)。将起源于此的参考资料一一核对肯定会花费很多时间。大家可以想一想,如果你是JVM开发者,你会怎么做?看完本章,你可能会和我一样,感叹JVM开发者的智慧。什么是根节点枚举?根节点枚举顾名思义就是找出所有的GCRoots。可以作为GCRoots的固定节点主要在全局引用(如常量或类静态属性)和执行上下文(如栈帧中的局部变量表)。可以作为GCRoots的固定对象包括如下(摘自《深入理解虚拟机 第3版》):虚拟机栈中引用的对象(栈帧中的局部变量表),比如各个线程调用的方法栈中使用的参数、局部变量、临时变量等方法区常量引用的对象,如字符串常量池(StringTable)中的引用。本地方法栈中JNI引用的对象(俗称Native方法)。Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻异常对象(如NullPointExcepiton、OutOfMemoryError)等,以及系统类加载器。同步锁(synchronized关键字)持有的所有对象。反映Java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存等。怀疑根节点枚举会面临与前面提到的内存碎片整理Hauntedby"StopTheWorld"类似的问题。根节点枚举必须在一致的快照中执行——这里的“一致性”是指执行子系统在整个枚举过程中似乎在某个时间点被冻结。你为什么要这样做?如果不“冻结”,根节点集合的对象引用关系是不断变化的,因此无法保证分析结果的准确性。因此,即使是CMS、G1和ZGC等声称具有可控暂停时间或(几乎)没有暂停的收集器,在枚举根节点的步骤中也必须暂停。如何解决根节点枚举的问题目前主流的Java虚拟机采用的是精准垃圾回收(精准GC使用的对象访问和定位方式是直接指针访问),所以当用户线程停止时,其实并不需要一个在无遗漏地检查了所有的执行上下文和全局引用位置之后,虚拟机应该有办法直接获取对象引用的存储位置。在HotSpot的解决方案中,一组称为OopMap的数据结构用于此目的。OopMap可以理解为一个映射表,存储了栈上对象引用的信息。这是一种用空间换取时间的做法。在枚举GCRoots时,只需要遍历每个栈帧的OopMap,通过OopMap中存储的信息快速找到GCRoots。说白了其实就是用一种类似于映射表的方法来记录引用关系,并且时不时地更新映射表,然后根节点枚举只需要扫描映射表就知道引用在哪里了存储,而不是执行全局扫描。安全点OK,问题又来了,既然OopMap是一个映射表,那么这个表什么时候更新呢?您必须知道引用关系的变化非常频繁。如果每次引用发生变化时都更新对应的OopMap,就会需要大量额外的存储空间,这样垃圾回收相关的空间成本就会变得高得无法承受。解决这个问题的方法是安全点,实际上,信息只是记录在“特定位置”,这些位置称为安全点(Safepoint)。所以GC并不是随时随地都来的,只有到了安全点才能开始GC。一般安全点选择在以下几个位置:循环结束,调用方法后方法抛出异常才返回,但是还有一个问题需要考虑:如何让所有线程运行到最近的位置当垃圾收集发生时的安全点点,然后暂停。有两个选项可供选择:先发制人暂停和自愿暂停。抢占式中断:不需要线程的执行代码主动配合。当垃圾收集发生时,系统首先中断所有用户线程。它一次又一次地中断,直到到达安全点。几乎没有虚拟机实现使用抢占式中断来挂起线程以响应GC事件。主动中断:当垃圾回收需要中断线程时,并不直接对线程进行操作,而是简单地设置一个标志位,每个线程在执行过程中都会主动轮询这个标志位。一旦发现中断标志为真,就主动在最安全的地方中断挂起。轮询标志与安全点重合的地方,加上所有对象创建等需要在Java堆上分配内存的地方,这是为了检查是否即将发生垃圾回收,避免没有足够的内存分配新的对象。安全区安全点的设计,看似完美解决了如何停止用户线程,但还是有问题。安全点机制保证程序在执行时,会在短时间内遇到可以进入垃圾回收过程的安全点。但是当程序“不执行”时呢?所谓程序不执行,就是没有分配处理器时间。典型的场景是用户线程处于Sleep状态或者Blocked状态。这时候线程无法响应虚拟机的中断请求,也无法去安全的地方中断并挂起自己。针对这种情况,JVM引入了安全区域(SafeRegion)来解决。安全区是指能够保证在某段代码中,引用关系不会发生变化。因此,在这个区域的任何地方开始垃圾收集都是安全的。我们也可以把安全区看成是被拉长了的安全点。当用户线程执行到安全区的代码时,会先标记自己已经进入安全区,这样虚拟机在这期间要发起垃圾回收时,就不需要关心这些已经进入安全区的线程宣布自己在安全区。.**当线程即将离开安全区时,会检查虚拟机是否完成了对根节点的枚举(或者垃圾回收过程中其他需要挂起用户线程的阶段)。如果完成了,线程就当什么都没发生,继续执行;否则它必须等到它收到可以离开安全区域的信号。如果本博客有什么错误和建议,欢迎留言指正。文章持续更新中,大家可以关注公众号第一时间阅读。
