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 基础:回收哪些内存/对象 引用计数算法 可达性分析算法 finalize()方法 HotSpot实现分析
转自:https://blog.csdn.net/tjiyu/article/details/53982412 1-1.为什么需要了解垃圾回收 目前内存的动态分配与内存回收技术已经相当成熟,但为什么还 ...
- 深入探究JVM之垃圾回收算法实现细节
@ 目录 前言 垃圾回收算法实现细节 根节点枚举 安全点 安全区域 记忆集和卡表 写屏障 并发的可达性分析 低延迟GC Shenandoah ZGC 总结 前言 本篇紧接上文,主要讲解垃圾回收算法的实 ...
- JAVA垃圾回收-可达性分析算法
在java中是通过引用来和对象进行关联的,也就是说如果要操作对象,必须通过引用来进行.那么很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收.不失一般性,如果一个对象没有任何引用与之关联 ...
- JVM可达性分析算法中,哪些可以作为 root ?
被启动类(bootstrap 加载器)加载的类和创建的对象: JavaStack 中的引用的对象 (栈内存中引用的对象): 方法区中静态引用指向的对象: 方法区中常量引用指向的对象: Native 方 ...
- 面试官:你说你熟悉jvm?那你讲一下并发的可达性分析
这是why技术的第35篇原创文章 上面这张图是我还是北漂的时候,在鼓楼附近的胡同里面拍的. 那天刚刚下完雨,路过这个地方的时候,一瞬间就被这五颜六色的门板和自行车给吸引了,于是拍下了这张图片.看到这张 ...
- JVM中垃圾回收机制如何判断是否死亡?详解引用计数法和可达性分析 !
因为热爱,所以坚持. 文章下方有本文参考电子书和视频的下载地址哦~ 这节我们主要讲垃圾收集的一些基本概念,先了解垃圾收集是什么.然后触发条件是什么.最后虚拟机如何判断对象是否死亡. 一.前言 我们 ...
- JVM:并发的可达性分析
当前主流编程语言的垃圾收集器基本上都是依靠可达性分析算法来判定对象是否存活的,可达性分析算法理论上要求全过程都基于一个能保障一致性的快照中才能够进行分析,这意味着必须全程冻结用户线程的运行. 在根节点 ...
- JVM(九):垃圾回收算法
JVM(九):垃圾回收算法 在本文中,我们将从概念模型的角度探讨 JVM 是如何回收对象,包括 JVM 是如何判断一个对象已经死亡,什么时候在哪里进行了垃圾回收,垃圾回收有几种核心算法,每个算法优劣是 ...
- 《深入理解Java虚拟机》第三章读书笔记(二)——HotSpot垃圾回收算法实现(OopMap,安全点安全区域,卡表,写屏障,三色标记算法)
系列文章目录和关于我 前面<深入理解Java虚拟机>第三章读书笔记(一)--垃圾回收算法我们学习了垃圾回收算法理论知识,下面我们关注下HotSpot垃圾回收算法的实现,分为以下几部分 对象 ...
- Jvm垃圾回收器(算法篇)
在<Jvm垃圾回收器(基础篇)>中我们主要学习了判断对象是否存活还是死亡?两种基础的垃圾回收算法:引用计数法.可达性分析算法.以及Java引用的4种分类:强引用.软引用.弱引用.虚引用.和 ...
随机推荐
- 聊聊 GPU 产品选型那些事
随着人工智能的飞速崛起,随之而来的是算力需求的指数级增加,CPU 已经不足以满足深度学习.大模型计算等场景的海量数据处理需求.GPU 作为一种强大的计算工具,无论是高性能计算.图形渲染还是机器学习领域 ...
- VUE首屏加载优化 性能优化分析插件安装分享
优化背景: 项目上线后 第一次进入项目要等待接近50s才能进入页面.一开始觉得是电脑配置问题或者网络问题.F12后发现加载资源过慢 其中一个chunk-***js文件有10m 加载了45s .我们使用 ...
- Windows之——pid为4的system进程占用80端口的解决办法
因为Apache无法启动的原因,用netstat命令查看了一下80端口是否被占用了,如下 C:\Users\Maple>netstat -ano | findstr 0.0.0.0:80 TCP ...
- 如何使用JavaScript 将数据网格绑定到 GraphQL 服务
前言 作为一名前端开发人员,GraphQL对于我们来说是令人难以置信的好用.它可以用来简化数据访问,这让我们的工作变得更加容易. 什么是 GraphQL?它是一个抽象层,位于任意数量的数据源之上,并为 ...
- C/C++ Zlib库封装MyZip压缩类
Zlib是一个开源的数据压缩库,提供了一种通用的数据压缩和解压缩算法.它最初由Jean-Loup Gailly和Mark Adler开发,旨在成为一个高效.轻量级的压缩库,其被广泛应用于许多领域,包括 ...
- 从根上理解elasticsearch(lucene)查询原理(1)-lucece查询逻辑介绍
大家好,我是蓝胖子,最近在做一些elasticsearch 慢查询优化的事情,通常用分析elasticsearch 慢查询的时候可以通过profile api 去分析,分析结果显示的底层lucene在 ...
- 芯片SDC约束 -复制保存
https://www.cnblogs.com/pcc-uvm/p/16996456.html?share_token=9651df97-e94c-4653-bf71-0a0fd6ca415e& ...
- LeetCode:不用加号的加法(位运算)
解题思路:位运算,只能用位运算符.a.b同号比较好处理.主要是异号的情况,考虑 a>0,b<0,因为 a,b的绝对值都不会超过2^32,因此取模数为2^32.根据同余方程可知 (a+b)% ...
- oracle12c静默安装
oracle12c 静默安装 先决条件 ● 至少 1 GB RAM 用于 Oracle 数据库安装.建议使用 2 GB 内存. ● 至少 8 GB RAM 用于 Oracle Grid Infrast ...
- 如何对连续型数据进行离散化处理,并进行OneHot编码?
如何对连续型数据进行离散化处理,并进行OneHot编码,最终将OneHot编码作为特征因子输入模型? 什么是OneHot编码 One-Hot编码是分类变量作为二进制向量的表示.这首先要求将分类值映射到 ...