JVM HotSpot 可达性分析算法实现细节
本文部分摘自《深入理解 Java 虚拟机第三版》
根节点枚举
在之前关于可达性分析算法的介绍中我们讲过,我们需要先找出可固定作为 GC Roots 的节点,然后沿着引用链去寻找那些无用的垃圾对象。GC Roots 节点一般在全局性引用(例如常量和类静态属性)与执行上下文(例如栈帧中的本地变量表)中,尽管目标明确,但查找过程要做到高效并非一件易事,若要逐个查找可作为起源的引用肯定需要消耗不少时间
迄今为止,所有收集器在根节点枚举这一步骤时都是必须暂停用户线程,也即 Stop The World,因为如果在分析过程中出现根节点集合中对象的引用关系仍在不断变化的情况,分析结果的准确性也就无法保证了
在对栈内存进行分析时,虚拟机会看哪些位置存储了 Reference 类型,如果发现某个位置确实存的是 Reference 类型,就意味着它所引用的对象这一次不能被回收。但问题是,栈帧的本地变量表里面只有一部分数据是 Reference 类型的,那些非 Reference 类型(基本数据类型)的数据对我们毫无用处,但我们还是不得不对整个栈全部扫描一遍,这是对时间和资源的一种浪费。在 HotSpot 的解决方案中采用了一组称为 OopMap 的数据结构来实现直接找到对象引用,一旦类加载动作完成,HotSpot 就会把栈中代表引用的位置全部记录下来,这样收集器在扫描时就可以直接得知这些消息了
安全点
尽管有了 OopMap,但如果引用关系经常变化,虚拟机就需要为每一条指令都生成对应的 OopMap,这将会占用大量的额外存储空间
HotSpot 当然没那么笨,它只会在特定的位置去记录这些信息,这些位置被称为安全点(SafePoint)。有了安全点的设定,用户程序就必须执行到安全点才能暂停,而不是在代码指令流的任意位置随意停顿。安全点的选定不能太少,让收集器等待时间过长,也不能太频繁,导致增大运行时内存负担。安全点的位置选定基本上是以“是否具有让程序长时间执行的特征”为标准进行选定,“长时间执行”的最明显特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等,只有具有这些功能的指令才能产生安全点
对于安全点,另外一个要考虑的问题就是,如何在垃圾收集发生时让所有线程都跑到最近的安全点。一般有两种方案可供选择:
- 抢先式中断:垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地点不在安全点上,就恢复该线程执行,直至跑到安全点再中断。现实中几乎没有虚拟机会采用抢先式中断
- 主动式中断:垃圾收集发生时,不直接对线程操作,而是设置一个标志位,各个线程在执行时会不停地主动去轮询这个标志,一旦发现标志位为真就在最近的安全点主动中断
安全区域
安全点看似解决了我们遇到的问题,但还有一个需要思考的点:如果某一个用户线程正好处于“不执行”状态该怎么办?所谓“不执行”就是没有分配处理器时间片,典型的场景如用户线程处于 Sleep 或 Blocked 状态,这时线程无法响应中断请求,自然也就不能走到安全点主动挂起自己,而虚拟机也不可能持续等待线程重新被分处理器时间片。对于这种情况,就需要引入安全区域(Safe Region)来解决
安全区域是指能够确保在某一代码片段中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作是被扩展拉伸了的安全点
当用户线程执行到安全区域时,首先会标识自己已经进入安全区域,那样当这段时间里虚拟机要发起垃圾收集时就不必去管这些已经声明自己在安全区域内的线程了。当线程要离开安全区域时,会检查虚拟机是否已经完成了根节点枚举,如果完成了,就继续执行,否则一直等待,直到收到可以离开安全区域的信号为止
记忆集与卡表
之前在讲解分代收集理论时,提到为了解决对象跨代引用的问题,垃圾收集器会在新生代建立名为记忆集(Remember Set)的数据结构,避免将整个老年代加入 GC Roots。事实上,所有涉及部分区域收集行为的垃圾收集器都会面临相同的问题
记忆集是一种用于记录非收集区域指向收集区域的指针集合的抽象数据结构,最简单的实现可以是数组,其中存放非收集区域中所有含跨代引用的对象。实际上,收集器只需要通过记忆集判断某一块非收集区域是否存在指向收集区域的指针即可,并不需要了解跨代指针的全部细节,因此我们可以适当选择更粗犷的记录粒度:
- 字长精度:每个记录精确到一个机器字长,该字包含跨代指针
- 对象精度:每个记录精确到一个对象,该对象里有字段含跨代指针
- 卡精度:每个记录精确到一块内存区域,该区域有对象含跨代指针
最常用的是第三种“卡精度”,使用一种称为“卡表”的方式去实现记忆集。这里要提的一点是,记忆集只是一种抽象的数据结构,卡表是记忆集的一种具体实现,两者的关系可以类比 Java 中的 Map 和 HashMap
卡表最简单的形式可以是一个字节数组,HotSpot 虚拟机也确实这么做了。字节数组的每一个元素都对应其标识的内存区域中一块特定大小的内存块,这个内存块称为“卡页”。一个卡页的内存通常包含不止一个元素,只要卡页内有一个或多个对象的字段存在跨代指针,那就将对应卡表的数组元素标识为 1,否则为 0。发生垃圾收集时,只要筛选出卡表中变脏的元素,就能轻易地把它们加入 GC Roots
写屏障
如何维护卡表元素呢?例如它们何时变脏,谁来把它们变脏等。何时变脏的答案很明显,只要有其他分代区域的对象引用了本区域对象,那么对应的卡表元素就应该变脏,变脏时间点原则上应该发生在引用类型字段赋值的那一刻
问题是如何变脏呢?HotSpot 虚拟机是通过写屏障(Write Barrier)技术来维护卡表状态的。写屏障可以看作是虚拟机对“引用类型字段赋值”这个动作的 AOP 切面,在引用对象赋值时会产生一个环形通知,供程序执行额外的动作。应用写屏障后,虚拟机就会为所有赋值操作生成对应的指令。尽管这个动作也会产生额外开销,但和 Minor GC 时扫描整个老年代相比根本不值一提
卡表在高并发场景下还会面临伪共享(False Sharing)问题。现代中央处理器的缓存系统是以缓存行(Cache Line)为单位存储的,当多线程修改互相独立的变量,而这些变量恰好共享同一缓存行,则会导致性能降低。如果所有卡表元素共享同一缓存行,那么更新时有可能会出现伪共享问题。一种简单的解决方案是先检查卡表标记,只有当该卡表元素未被标记过时才将其标记为变脏
并发的可达性分析
可达性分析算法理论上要求全过程都基于一个能保障一致性的快照,即必须冻结全部用户线程。在根结点枚举阶段,由于 GC Roots 相比整个 Java 堆的全部对象毕竟还算极少数,且有各种优化技巧(如 OopMap),它带来的停顿可以说微不足道。但如果从 GC Roots 开始往下遍历对象图,那么这一阶段的停顿时间必然与 Java 堆容量成正比例关系:堆越大,存储的对象就越多,对象图结果越复杂,自然花的时间也越多
因此,部分垃圾收集器是允许用户线程与收集器线程并发工作的,但如果在收集器标记对象的同时,用户线程修改了引用关系,就会产生两种后果:把原本应该消亡的对象错误标记为存活;把原本应该存活的对象错误标记为消亡。前一种还好一些,不过是产生浮动垃圾罢了,而后一种就非常致命了,程序肯定会因此发生错误。为了更好地说明这个问题,我们按照“是否访问过”为条件将对象标记为以下三种颜色:
- 白色:表示对象尚未被垃圾收集器访问过
- 黑色:表示对象已经被垃圾收集器访问过,且该对象的所有引用都已经被扫描
- 灰色:表示对象已经被垃圾收集器访问过,但该对象至少还有一个引用没有被扫描
前面提到过的将应该存活的对象错误标记为消亡这一现象称为“对象消失”问题,即原本应该是黑色的对象被误标为白色,这一问题当且仅当以下两个条件同时满足时才会发生:
- 赋值器插入一条或多条从黑色对象到白色对象的新引用
- 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
要想解决对象消失问题,只需破坏这两个条件的任意一个即可,由此产生了两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning,SATB)
增量更新破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用,就将其记录下来,等并发扫描结束后,再将记录过的新引用关系中的黑色对象作为根,重新扫描一次
原始快照破坏的是第二个条件,当灰色对象要删除指向白色对象的引用时,同样将其记录下来,等并发扫描结束后,再将记录过的引用关系中的灰色对象为根,重新扫描一次
以上两种方式都是基于写屏障实现
JVM HotSpot 可达性分析算法实现细节相关推荐
- JVM:可达性分析算法
在堆里存放着几乎多有的java对象实例,垃圾搜集器在对堆进行回收之前,第一件事情就是确定这些对象之中哪些还"存活"着(即通过任何途径都无法使用的对象). 一.可达性分析算法 在Ja ...
- 引用计数器法 可达性分析算法_面试官:你说你熟悉jvm?那你讲一下并发的可达性分析...
持续输出原创文章,点击蓝字关注我吧 上面这张图是我还是北漂的时候,在鼓楼附近的胡同里面拍的. 那天刚刚下完雨,路过这个地方的时候,一瞬间就被这五颜六色的门板和自行车给吸引了,于是拍下了这张图片.看到这 ...
- 深入理解JVM(三)——JVM之判断对象是否存活(引用计数算法、可达性分析算法,最终判定),Eclipse设置GC日志输出,引用
本文转载自https://blog.csdn.net/ochangwen/article/details/51406779 本文是基于周志明的<深入理解Java虚拟机> 堆中几乎存放着Ja ...
- JVM 垃圾回收算法 -可达性分析算法!!!高频面试!!!
前言:学习JVM,那么不可避免的要去了解JVM相关的垃圾回收算法,本文只是讲了讲了可达性分析算法,至于标记-清除.标记-复制,标记-整理,分代收集等等算法,会在近两天的文章中陆续更新出来. 很喜欢一句 ...
- JVM——引用计数算法与可达性分析算法
前几篇博客我们一起认识了JVM的内存模型(程序计数器.虚拟机栈.本地方法栈.方法区与堆),了解了它们的内存结构与分配,同时也略带提到关于内存的回收. JVM--内存模型(一):程序计数器 JVM--内 ...
- 【JVM】引用计数和可达性分析算法详解
前言 JVM堆中几乎存放了所有对象的实例,那么垃圾收集器怎么确定哪些对象还"存活"着,哪些已经"死去"呢?本文主要介绍判断对象是否存活算法引用计数算法和可达性分 ...
- JVM—引用计数和可达性分析算法(存活性判断)
1 引用计数算法 1.1 算法思想 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1: 当引用失效时,计数器值就减1: 任何时候计数器为0时的对象就是不能再被使用. 1. ...
- 3.内存分配、逃逸分析与栈上分配、直接内存和运行时常量池、基本类型的包装类和常量池、TLAB、可达性分析算法(学习笔记)
3.JVM内存分配 3.1.内存分配概述 3.2.内存分配–Eden区域 3.3.内存分配–大对象直接进老年代 3.3.1.背景 3.3.2.解析 3.4.内存分配–长期存活的对象进去老年代 3.5. ...
- 深入理解JVM03--判断对象是否存活(引用计数算法、可达性分析算法,最终判定),Eclipse设置GC日志输出,引用
本文是基于周志明的<深入理解Java虚拟机> 堆中几乎存放着Java世界中所有的对象实例,垃圾收集器在对堆回收之前,第一件事情就是要确定这些对象哪些还"存活"着,哪些对 ...
- 【Java 虚拟机原理】垃圾回收算法 ( 可达性分析算法 | GC Root 示例 | GC 回收前的两次标记 | finalize 方法示例 )
文章目录 一.可达性分析算法 二.GC Root 示例 三.GC 回收前的两次标记 四.finalize 方法示例 一.可达性分析算法 在 堆内存 中 , 存在一个 根对象 GC Root , GC ...
最新文章
- Squid3反向代理安装与配置
- CSS选择器和参考手册
- Java库可以软件著作权,(最新整理)软件著作权-源代码范本
- Java面试之Java基础知识第一季
- 我的渣渣java实训
- Python画图(直方图、多张子图、二维图形、三维图形以及图中图)
- 在网页中嵌入任意字体的解决方案---google在线字体库应用
- python 解析json typeerror_TypeError:在使用Python解析JSON时,字符串索引必须是整数?...
- 需要多少次找出较轻的小球
- 一个对iBatis的总结写的不错(转载)
- linux查看内存条pn,内存条,详细教您怎么查看内存条的型号
- 计算机视觉与医疗PPT,图像理解与计算机视觉经典案例.ppt
- 各浏览器flash插件下载地址
- PHP获取一年有多少周和每周开始和结束日期
- 支付宝周期扣款Java逻辑代码
- 字符画——ASCII art
- Kafka Broker
- 前端那些你必须了解的知识点
- 4G,让这个冬天不寒冷
- 详情小三角css,CSS实现小三角