关于java:GC的前置工作聊聊GC是如何快速枚举根节点的

32次阅读

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

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

微信公众号:Java 随想录

原创不易,重视版权。转载请注明原作者和原文链接

上篇文章中咱们留下了个坑:「根节点枚举」,这篇文章就把坑填上。

在上篇文章中咱们晓得了 HotSpot 应用的是可达性剖析算法,该算法须要进行根节点枚举。

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

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

后面的文章大伙可能有点忘了,那么首先咱们对根节点枚举,先做个温习(我相对不是在混字数)。

什么是根节点枚举

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

当然要成为 GC Roots 是有条件的,固定可作为 GC Roots 的对象包含以下几种(摘抄自《深刻了解虚拟机 第 3 版》):

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

下面说的这些,大伙必定记不住,反正总结就一句话:固定可作为 GC Roots 的节点次要在全局性的援用(例如常量或类动态属性)与执行上下文(例如栈帧中的本地变量表)中。

根节点枚举存在的问题

迄今为止,所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的。因而毫无疑问根节点枚举与之前提及的整顿内存碎片一样会面临类似的「Stop The World」的困扰。

根节点枚举必须在一个能保障一致性的快照中才得以进行——这里「一致性」的意思是整个枚举期间执行子系统看起来就像被解冻在某个工夫点上。

为什么要这么做?

试想一下,你妈给你清扫房间,你妈一边清扫,你一边丢垃圾,房间永远也清扫不洁净。

所以实质上来说,根节点枚举遇到的问题,就是并发问题。

如果不「解冻」的话,根节点汇合的对象援用关系在一直变动,那么剖析后果准确性也就无奈保障。

所以即便是号称进展工夫可控,或者(简直)不会产生进展的 CMS、G1、ZGC 等收集器,在枚举根节点这一步也是必须要进展的。

弄明确问题之后,咱们开动脑筋想想怎么解决。

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

目前支流 Java 虚拟机应用的都是「精确式垃圾收集」。

所谓精确式垃圾收集是指垃圾收集器可能准确地确定内存中哪些区域被对象援用,哪些区域曾经不再应用,并且能够立刻回收不再应用的内存。

在精确式垃圾收集中,垃圾收集器须要晓得每一个援用类型变量(包含实例字段、动态字段、本地变量和输出参数等)在内存中的确切地位,以及这个地位是否正在被援用。

这样,当垃圾收集器须要进行回收时,它就能够准确地找到并回收那些不再有任何援用的对象所占用的内存。

绝对应的,还有一种叫做「激进式垃圾收集」,它不能准确地辨认所有的援用,只能激进地认为所有看起来像对象援用的值都可能是援用。这种形式可能会导致某些实际上能够被回收的内存得不到回收。

HotSpot 采纳的是精确式垃圾收集

所以当用户线程停顿下来之后,其实并不需要一个不漏地查看完所有执行上下文和全局的援用地位,虚拟机该当是有方法间接失去哪些地方寄存着对象援用的。

在 HotSpot 的解决方案里,是应用一组称为 OopMap 的数据结构来达到这个目标。OopMap 能够了解为就是映射表,存储栈上的对象援用的信息,这是一种空间换工夫的做法。

在 GC Roots 枚举时,只须要遍历每个栈桢的 OopMap,通过 OopMap 存储的信息,快捷地找到 GC Roots,这样就不须要进行全局扫描。

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

OK,弄明确之后,问题又来了,既然 OopMap 是一个映射表,这个表什么时候被更新?

你可能会感觉这有啥难的,援用更新的时候同步去更新映射表不就完事了吗,然而事件并没有想的那么简略。

要晓得援用关系变动是非常频繁的,如果援用每变动一次就更新对应的 OopMap,那将会须要大量的额定存储空间,这样垃圾收集随同而来的空间老本就会变得无法忍受的昂扬。

平安点

解决这个问题的方法就是「平安点」,事实上,只是在「特定的地位」记录了这些信息,这些地位被称为平安点(Safepoint)。

因而 GC 不是随时随地来的,失去达平安点时才能够开始 GC。

所以流程咱们就分明了:先是达到平安点,而后更新 OopMp,而后进行根节点枚举,找到GC Roots,开始 GC。

平安点的选举,个别会在如下几个地位呈现:

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

到这里为止,貌似问题咱们都解决了,but,还有一个问题咱们须要思考,咱们后面说了零碎要在某个工夫点处于「解冻」状态,那么如何在垃圾收集产生时让所有线程都跑到最近的平安点,而后停顿下来?

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

  • 「领先式中断」:不须要线程的执行代码被动去配合,在垃圾收集产生时,零碎首先把所有用户线程全副中断,如果发现有用户线程中断的中央不在平安点上,就复原这条线程执行,让它一会再从新中断,直到跑到平安点上。当初简直没有虚拟机实现采纳领先式中断来暂停线程响应 GC 事件。
  • 「主动式中断」:当垃圾收集须要中断线程的时候,不间接对线程操作,仅仅简略地设置一个标记位,各个线程执行过程时会不停地被动去轮询这个标记,一旦发现中断标记为真时就本人在最近的平安点上被动中断挂起。

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

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

对于这种状况,JVM 引入 平安区域(Safe Region)来解决。

平安区域

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

当用户线程执行到平安区域外面的代码时,首先会标识本人曾经进入了平安区域。

那样当这段时间里虚拟机要发动垃圾收集时就不用去管这些已申明本人在平安区域内的线程了。

当线程要来到平安区域时,它要查看虚拟机是否曾经实现了根节点枚举(或者垃圾收集过程中其余须要暂停用户线程的阶段)。

如果实现了,那线程就当作没事产生过,继续执行;否则它就必须始终期待,直到收到能够来到平安区域的信号为止。

好了,本篇文章到这完结咯。联合上篇可达性剖析,咱们一步步揭开了 GC 的神秘面纱,然而路还很远,依然还有很多货色是须要咱们去学习理解的。


感激浏览,如果本篇文章有任何谬误和倡议,欢送给我留言斧正。

老铁们,关注我的微信公众号「Java 随想录」,专一分享 Java 技术干货,文章继续更新,能够关注公众号第一工夫浏览。

一起交流学习,期待与你共同进步!

正文完
 0