关于后端:聊聊GC是如何快速枚举根节点的

52次阅读

共计 2481 个字符,预计需要花费 7 分钟才能阅读完成。

本文已收录至 Github,举荐浏览 👉 Java 随想录

微信公众号:Java 随想录

CSDN:码农 BookSea

世界上最高兴的事,莫过于为现实而奋斗。——苏格拉底

HotSpot 应用的是可达性剖析算法,该算法须要进行根节点枚举,查找根节点枚举的过程要做到高效并非一件容易的事件,当初 Java 利用越做越宏大,光是办法区的大小就常有数百上千兆,外面的类、常量等更是 恒河沙数(一种修辞手法),若要一一查看以这里为起源的援用必定得耗费不少工夫。

大家能够思考下,如果你是 JVM 的开发者,你会怎么去做?

看完这一章节,你或者会跟我一样,感叹 JVM 开发者的智慧。

什么是根节点枚举

顾名思义,根节点枚举就是找出所有的GC Roots

固定可作为 GC Roots 的节点次要在全局性的援用(例如常量或类动态属性)与执行上下文(例如栈帧中的本地变量表)中。

固定可作为 GC Roots 的对象包含以下几种(摘抄自《深刻了解虚拟机 第 3 版》):

  • 在虚拟机栈(栈帧中的本地变量表)中援用的对象,譬如各个线程被调用的办法堆栈中应用到的参数、局部变量、长期变量等。
  • 在办法区中常量援用的对象,譬如字符串常量池(String Table)里的援用。
  • 在本地办法栈中 JNI(即通常所说的 Native 办法)援用的对象。
  • Java 虚拟机外部的援用,如根本数据类型对应的 Class 对象,一些常驻的异样对象(比方 NullPointExcepiton、OutOfMemoryError)等,还有零碎类加载器。
  • 所有被同步锁(synchronized 关键字)持有的对象。
  • 反映 Java 虚拟机外部状况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。

根节点枚举存在的问题

迄今为止,所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的,因而毫无疑问根节点枚举与之前提及的整顿内存碎片一样会面临类似的“Stop The World”的困扰。根节点枚举必须在一个能保障一致性的快照中才得以进行——这里“一致性”的意思是整个枚举期间执行子系统看起来就像被解冻在某个工夫点上。

为什么要这么做?

如果不”解冻”的话,根节点汇合的对象援用关系在一直变动,那么剖析后果准确性也就无奈保障。所以即便是号称进展工夫可控,或者(简直)不会产生进展的 CMS、G1、ZGC 等收集器,在枚举根节点这一步也是必须要进展的。

如何解决根节点枚举的问题

目前支流 Java 虚拟机应用的都是精确式垃圾收集(精确式 GC 应用的对象拜访定位形式是间接指针拜访 ),所以当用户线程停顿下来之后,其实并不需要一个不漏地查看完所有执行上下文和全局的援用地位,虚拟机该当是有方法间接失去哪些地方寄存着对象援用的。
在 HotSpot 的解决方案里,是应用一组称为 OopMap 的数据结构来达到这个目标。OopMap 能够了解为就是映射表,存储栈上的对象援用的信息,这是一种空间换工夫的做法。在 GC Roots 枚举时,只须要遍历每个栈桢的 OopMap,通过 OopMap 存储的信息,快捷地找到 GC Roots。

用大白话说,其实就是用相似映射表这种伎俩记录下来援用关系,时不时去更新下映射表,而后根节点枚举只须要扫描映射表就晓得哪些地方寄存援用了,而不必去进行全局扫描。

平安点

OK,问题又来了,既然 OopMap 是一个映射表,这个表什么时候被更新?要晓得援用关系变动是非常频繁的,如果援用每变动一次就更新对应的 OopMap,那将会须要大量的额定存储空间,这样垃圾收集随同而来的空间老本就会变得无法忍受的昂扬。

解决这个的方法就是 平安点 ,事实上,只是在“特定的地位”记录了这些信息, 这些地位被称为平安点(Safepoint)。因而 GC 不是随时随地来的,失去达平安点时才能够开始 GC

个别会在如下几个地位抉择平安点:

  • 循环的开端
  • 办法临返回前
  • 调用办法之后
  • 抛异样的地位

然而还有一个问题是须要思考的:如何在垃圾收集产生时让所有线程都跑到最近的平安点,而后停顿下来。

有两种计划可供选择:领先式中断(Preemptive Suspension) 主动式中断(Voluntary Suspension)

  • 领先式中断:不须要线程的执行代码被动去配合,在垃圾收集产生时,零碎首先把所有用户线程全副中断,如果发现有用户线程中断的中央不在平安点上,就复原这条线程执行,让它一会再从新中断,直到跑到平安点上。当初简直没有虚机实现采纳领先式中断来暂停线程响应 GC 事件。
  • 主动式中断:当垃圾收集须要中断线程的时候,不间接对线程操作,仅仅简略地设置一个标记位,各个线程执行过程时会不停地被动去轮询这个标记,一旦发现中断标记为真时就本人在最的平安点上被动中断挂起。轮询标记的中央和平安点是重合的,另外还要加上所有创建对象和其余须要在 Java 堆上分配内存的中央,这是为了查看是否行将要产生垃圾收集,防止没有足够内存调配新对象。

平安区域

平安点的设计仿佛曾经完满解决如何进展用户线程,然而依然有问题,平安点机制保障了程序执行时,在不太长的工夫内就会遇到可进入垃圾收集过程的平安点。然而,程序“不执行”的时候呢?

所谓的程序不执行就是没有调配处理器工夫,典型的场景便是用户线程处于 Sleep 状态或者 Blocked 状态,这时候线程无奈响应虚拟机的中断请求,不能再走到平安的中央去中断挂起本人。对于这种状况,JVM 引入 平安区域(Safe Region)来解决。

平安区域是指可能确保在某一段代码片段之中,援用关系不会发生变化,因而,在这个区域中任意中央开始垃圾收集都是平安的。咱们也能够把平安区域看作被扩大拉伸了的平安点。

当用户线程执行到平安区域外面的代码时,首先会标识本人曾经进入了平安区域,那样当这段时间里虚拟机要发动垃圾收集时就不用去管这些已申明本人在平安区域内的线程了。** 当线程要来到平安区域时,它要查看虚拟机是否曾经实现了根节点枚举(或者垃圾收集过程中其余须要暂停用户线程的阶段),如果实现了,那线程就当作没事产生过,继续执行;否则它就必须始终期待,直到收到能够来到平安区域的信号为止。


如果本篇博客有任何谬误和倡议,欢送给我留言斧正。文章继续更新,能够关注公众号第一工夫浏览。

正文完
 0