共计 2725 个字符,预计需要花费 7 分钟才能阅读完成。
本文已收录至 Github,举荐浏览 👉 Java 随想录
微信公众号:Java 随想录
CSDN:码农 BookSea
转载请在文首注明出处,如发现歹意剽窃 / 搬运,会动用法律武器保护本人的权利。让咱们一起保护一个良好的技术创作环境!
根节点枚举与平安点
什么是根节点枚举
HotSpot 应用的是可达性剖析算法,该算法基本思路如下:
- 可达性剖析算法是以根对象汇合(GCRoots)为起始点,依照从上至下的形式搜寻被根对象汇合所连贯的指标对象是否可达。
- 应用可达性剖析算法后,内存中的存活对象都会被根对象汇合间接或间接连贯着,搜寻所走过的门路称为援用链(Reference Chain)。
- 如果指标对象没有任何援用链相连,则是不可达的,就意味着该对象己经死亡,能够标记为垃圾对象。在可达性剖析算法中,只有可能被根对象汇合间接或者间接连贯的对象才是存活对象。
顾名思义,根节点枚举就是找出所有的GC Roots
。
固定可作为 GC Roots 的节点次要在全局性的援用(例如常量或类动态属性)与执行上下文(例如栈帧中的本地变量表)中。
固定可作为 GC Roots 的对象包含以下几种(摘抄自《深刻了解虚拟机 第 3 版》):
- 在虚拟机栈(栈帧中的本地变量表)中援用的对象,譬如各个线程被调用的办法堆栈中应用到的参数、局部变量、长期变量等。
- 在办法区中常量援用的对象,譬如字符串常量池(String Table)里的援用。
- 在本地办法栈中 JNI(即通常所说的 Native 办法)援用的对象。
- Java 虚拟机外部的援用,如根本数据类型对应的 Class 对象,一些常驻的异样对象(比方 NullPointExcepiton、OutOfMemoryError)等,还有零碎类加载器。
- 所有被同步锁(synchronized 关键字)持有的对象。
- 反映 Java 虚拟机外部状况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。
根节点枚举存在的问题
查找根节点枚举的过程要做到高效并非一件容易的事件,当初 Java 利用越做越宏大,光是办法区的大小就常有数百上千兆,外面的类、常量等更是恒河沙数,若要一一查看以这里为起源的援用必定得耗费不少工夫。
<font color=red> 迄今为止,所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的 </font>,因而毫无疑问根节点枚举与之前提及的整顿内存碎片一样会面临类似的“Stop The
World”的困扰。根节点枚举必须在一个能保障一致性的快照中才得以进行——这里“一致性”的意思是整个枚举期间执行子系统看起来就像被解冻在某个工夫点上,
不会呈现剖析过程中,根节点汇合的对象援用关系还在一直变动的状况,<font color=red> 若这点不能满足的话,剖析后果准确性也就无奈保障。</font> 这是导致垃圾收集过程必须进展所有用户线程的其中一个重要起因,即便是号称进展工夫可控,或者(简直)不会产生进展的 CMS、G1、ZGC 等收集器,枚举根节点时也是必须要进展的。
如何解决根节点枚举的问题
目前支流 Java 虚拟机应用的都是精确式垃圾收集(精确式 GC 应用的对象拜访定位形式是间接指针拜访),所以当用户线程停顿下来之后,其实并不需要一个不漏地查看完所有执行上下文和全局的援用地位,虚拟机该当是有方法间接失去哪些地方寄存着对象援用的。
在 HotSpot 的解决方案里,是应用一组称为 OopMap
的数据结构来达到这个目标。<font color=red>OopMap 能够了解为就是映射表,存储栈上的对象援用的信息,这是一种空间换工夫的做法,在 GC Roots 枚举时,只须要遍历每个栈桢的 OopMap,通过 OopMap 存储的信息,快捷地找到 GC Roots。</font>
平安点
OK,问题又来了,既然 OopMap 是一个映射表,这个表什么时候被更新?要晓得援用关系变动是非常频繁的,如果援用每变动一次就更新对应的 OopMap,那将会须要大量的额定存储空间,这样垃圾收集随同而来的空间老本就会变得无法忍受的昂扬。
解决这个的方法就是平安点,事实上,只是在“特定的地位”记录了这些信息,这些地位被称为平安点(Safepoint)。有了平安点的设定,也就决定了用户程序执行时并非在代码指令流的任意地位都可能停顿下来开始垃圾收集,而是强制要求必须执行达到平安点后才可能暂停,平安点地位的选取基本上是以“是否具备让程序
长时间执行的特色”为规范进行选定的。然而还有一个问题是须要思考的,如何在垃圾收集产生时让所有线程都跑到最近的平安点,而后停顿下来。
有两种计划可供选择:领先式中断(Preemptive Suspension)
和 主动式中断(Voluntary Suspension)
。
领先式中断不须要线程的执行代码被动去配合,在垃圾收集产生时,零碎首先把所有用户线程全副中断,如果发现有用户线程中断的中央不在平安点上,就复原这条线程执行,让它一会再从新中断,直到跑到平安点上。当初简直没有虚机实现采纳领先式中断来暂停线程响应 GC 事件。
主动式中断的思维是当垃圾收集须要中断线程的时候,不间接对线程操作,仅仅简略地设置一个标记位,各个线程执行过程时会不停地被动去轮询这个标记,一旦发现中断标记为真时就本人在最的平安点上被动中断挂起。轮询标记的中央和平安点是重合的,另外还要加上所有创建对象和其余须要在 Java 堆上分配内存的中央,这是为了查看是否行将要产生垃圾收集,防止没有足够内存调配新对象。
平安区域
平安点的设计仿佛曾经完满解决如何进展用户线程,然而依然有问题,<font color=red> 平安点机制保障了程序执行时,在不太长的工夫内就会遇到可进入垃圾收集过程的平安点。然而,程序“不执行”的时候呢?</font>
所谓的程序不执行就是没有调配处理器工夫,典型的场景便是用户线程处于 Sleep 状态或者 Blocked 状态,这时候线程无奈响应虚拟机的中断请求,不能再走到平安的中央去中断挂起本人,虚拟机也显然不可能继续期待线程从新被激活调配处理器工夫。对于这种状况,就必须引入 平安区域(Safe Region)
来解决。
平安区域是指可能确保在某一段代码片段之中,援用关系不会发生变化,因而,在这个区域中任意中央开始垃圾收集都是平安的。咱们也能够把平安区域看作被扩大拉伸了的平安点。
当用户线程执行到平安区域外面的代码时,首先会标识本人曾经进入了平安区域,那样当这段时间里虚拟机要发动垃圾收集时就不用去管这些已申明本人在平安区域内的线程了。当线程要来到平安区域时,它要查看虚拟机是否曾经实现了根节点枚举(或者垃圾收集过程中其余须要暂停用户线程的阶段),如果实现了,那线程就当作没事产生过,继续执行;否则它就必须始终期待,直到收到能够来到平安区域的信号为止。