JVM 垃圾回收算法细节

目录

前言

GC Root 可达性分析

根节点枚举

安全点        

安全区域

记忆集与卡表

写屏障

并行的可达性分析


前言

        学习了几种垃圾收集算法之后, 我们再来看看它们在具体实现上有什么细节之处,我们所能看到的理论很简单,但是实现起来那就是另外一回事了。

GC Root 可达性分析

        我们之前讲的,可达性分析,就是一个非常不错的,用于查看一个对象是否需要被消灭(从内存中删除)的方法,也可以说看一个对象是否存活的方法。

        可达性分析法从GC Root开始,往下遍历,根据引用的关系向下搜索,搜索过的路径被称为引用链,如下: 

        图中的连线你可以理解为引用链,这个结构类似树形结构,不在这个任何一个引用链上的对象,就可以被判定为一个死亡的对象。

        那么前面也提到过,哪些东西可以作为GC Root呢?我们来仔细思考一下,而不是直接翻阅别的文档,例如一个线程执行到了方法调用的这段代码之后,就会产生一个新的函数栈帧,然后在被调用的方法中,会生成局部变量,这个局部变量可能是一个引用变量,如下: 

public class Demo {  // 定义一个方法,该方法接受一个Person类型的引用变量  public static void func() {  // person的引用Person person = new Person("Alice", 30);  System.out.println("Name: " + person.getName());  System.out.println("Age: " + person.getAge());  }  public static void main(String[] args) {  introducePerson(person);  }  
}

        其中调用func()的时候,就创建了对应的函数栈帧和局部变量表,这个局部变量表中就包含了引用变量"person", 此时,person这个引用变量就可以作为一个GC Root,那么此时"new Person("Alice", 30);  ", 也就是在内存中真实存在的实例对象,它就被当前的person引用变量作为GC Root的引用链连接着,因此在还没有出栈的时候,person引用变量指向的对象一直是存活的,直到方法出栈,引用变量person被释放,那么就没有变量引用这个person真实的对象了,因此也就没有跟任何引用链相关联,因此在不久后就会被垃圾收集器回收。

        当然不只是局部变量,还有如下: 

  • java虚拟机战中的引用的对象:参数,临时变量,包括局部变量
  • 方法区中引用的对象,例如字符串常量池
  • 本地方法栈同java虚拟机栈
  • 。。。。不一一列举

根节点枚举

        从上述的可行性分析的角度来看,我们思考一个问题: 

  • 我如何知道一个对象是否没有任何引用链与之关联?

        其实可以想到,我们把所有的可以作为GC Root的东西拿出来放在一个list或者一个集合,我们称之为GC Roots, 或者GC Root List(Set), 然后去遍历这个集合中的每一个引用链,如果发现存在引用关系,那么他就是存在引用链与之关联,反之即为死亡对象。

        但是这样做必然存在一个问题,那就是这样的遍历速度会非常慢,尽管你已经知道要在哪里找了,但是查找的过程依然不是一个简单的过程,现在的java程序,做大了光是方法区就有几百上千兆,里面的类和常量更是不计其数。逐个检查以这里为起源的引用链就要花费不少力气。我们把这种暴力遍历的行为称之为根节点枚举

        不光如此,加上你在遍历的时候,如果用户线程是运行的状态,那么几乎是必然会产生变量引用改变的问题,你对一个对象进行了根节点枚举,此时发现它存在相关引用链,因此判定为存活,但是在用户线程运行期间,他可能会因为方法出栈而被取消引用,从而丧失当前的引用链关系,从而变成下一次GC的对象。那么当前的GC统计结果也就不正确了。

        因此根节点枚举,这个步骤中,用户线程必然是停止的,这也是导致垃圾收集必然会停顿的所有用户线程的一个重要原因。

        如何解决?请思考片刻,然后继续往下看。

        正向找很难找,为什么不逆向试试?上上届我们学过,在java中,编译的所占的内存是固定的,并且我可以在编译的时候,就知道哪里存存在某个对象的引用变量,列举一个例子,那现在有一个Person类,如下: 

public class Person {  private String name;  private int age;  public Person(String name, int age) {  this.name = name;  this.age = age;  }  // 假设还有其他方法...  
}

        在编译一个方法的时候,发现这个方法里面存在一个对Person对象的引用变量,因此它就会对其进行记录, 我们称之为OopMap,如下: 

指令位置寄存器/栈槽引用类型
0x0120栈槽0Person

        这个记录告诉我们,在指令位置为0x0120的时候,栈槽0中,存储了一个Person类型的引用变量,这里的指令位置是指的编译后产生的字节码中的偏移量,在垃圾手机的时候,当JVM需要进行可达性分析的时候,它会查找当前线程执行到的字节码的位置,并检查是否与之有对应的OopMap。例如在运行的时候你需要确定堆中的一个person的对象是否存在引用链,你就可以去扫描OopMap,发现指令位置为0x0120的地方存在对Person的引用,一旦你找到了包含0x0120指令的方法接下来就需要分析该方法的局部变量表(Local Variables)和操作数栈(Operand Stack)。

        扫描每个线程的虚拟机栈,找到对应的栈帧中的存在的对person对象中的引用。进行扫描即可,所以避免了为了一个对象的存活去扫描整个GC Root List;

        


安全点        

        前面提到可以使用OopMap来极大的缩小停顿的时间,但是引用的变化会引起OopMap的频繁更新和新增,那么就会额外消耗内存,在进行垃圾回收之前,JVM需要确保没有线程在对对象进行修改,以避免在回收过程中错误地标记或删除仍在使用的对象。安全点允许JVM暂停所有线程,从而确保垃圾回收器能够安全地执行。

        在HotSpot JVM中,安全点主要在以下位置设置:

  • 方法调用:在方法调用之前和之后插入安全点,确保方法调用过程中的状态一致性。
  • 循环末尾:对于长时间运行的循环,在循环的末尾设置安全点,以减少线程长时间无法进入安全点导致的GC停顿时间过长。
  • 异常处理:在异常抛出和处理过程中设置安全点,确保异常处理过程中的状态一致性。
  • 同步块:在进入和退出同步块时设置安全点,保护同步块内的资源不被非法访问或修改。

        如何让所有的线程都到安全点的地方去?

        有的线程可能到达了自己的安全点,有的线程可能没有到达自己的安全点,此时有两种方案可以选择: 

  • 抢占式: 不需要线程的执行代码主动去配合,在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上
  • 主动式:不直接对线程,各个线程执操作,仅仅简单地设置一个标志位行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起

        轮询这个操作必须足够高效(因为涉及到很多线程),HotSpot使用内存保护陷阱的方式来实现。



安全区域

        现在所有的线程都到达自己的安全点了,那么就进入了GC的状态了。但是实际情况缺不一定,想想为什么,学过多线程的都知道,如果一个线程处于阻塞或者是睡眠状态,那么这个线程就无法轮询当前的标志位,也就无法得知自己是否需要在下一个安全点停下来。不能再继续走到安全的地方去挂断自己,显然也不可能等待被阻塞的线程被重新激活然后分配处理器时间。

        就必须使用另外一种方法解决它。那就是安全区域,一个线程进入安全区域之后,是不允许操作对象和其引用的,因此即使没有响应垃圾收集到安全点挂断自己,也可以在其中进行垃圾收集。收集完毕之后,就会收到信号,线程就可以离开这个安全区域。



记忆集与卡表

        为了解决对象的跨代引用的问题,垃圾收集器在新生代中建立了名为记忆集的数据结构,避免为了一个对象存在跨代引用而将老年代整个加入GC Roots,记忆集是一种,记录从非收集区域指向手机区域的指针集合的数据结构。

        比如我们可以将存在跨代引用的老年代对象的引用归为一个集合,然后如果需要回收的对象,只需要扫描这个记忆集,就可以快速知道哪些对象存在跨代引用,这种记录的方式会非常消耗内存,因此在垃圾手机的时候,只需要通过记忆集,来得知某一块内存区域中存在老年代的引用新生代的情况即可。

        当然你还有其他的精度可以选择:

  • 字长精度 : 每个记录精确到一个字长, 该字包含了跨代指针
  • 对象精度: 每个记录精确到一个对象, 该对象里面有跨代的指针
  • 卡精度: 也就是每个记录精确到一个内存区域, 

        我们只用扫描特定的返回即可,不必一个对象一个对象的搜索.

        我们详细说说卡表的形式. 卡表的最简单的形式只是一个字节数组, 每一个字节数组的每一个元素都对应标识着一个内存块, 这个内存块也被称为卡页, 每个卡页一般是2的N次幂的字节数, 如果每个卡页的大小为512字节的话, 如果卡表的表示区域的起始地址是0x0000的话, 那么一个卡页就会往后偏移512字节的大小, 卡页1对应的内存的地址范围就为0x0000 ~ 0x01FFF. 以此类推: 

        这一个卡页里面, 通常不只是包含一个对象, 同时, 这个卡页标记的内存区域, 只要有一个对象存在跨代引用, 那么对应的标志位就会被置为1. 没有则标为0, 在GC的时候, 也就只会扫描卡表标志位为1的卡页, 从而避免了扫描整个老年代.



写屏障

        从最开始的对象的可达性分析, 通过OopMap来避免扫描整个GC Roots的方法减少根节点枚举的时间, 到后面的进行GC的时候为了避免用户线程的执行导致的枚举的状态信息数据变脏从而影响数据一致性, 到现在的通过安全区域防止没有接收到中断指令的线程修改对象引用. 

        上届我们讲述了记忆集和课表, 其作用就是为了快速的找出那些跨代引用的老年代对象, 从而避免将整个老年代加入到GC Roots中进行扫描.

        卡表示记忆集的一种实现, 它将内存分文为卡页, 然后通过标记区域的方式, 来说明某个区域是否可能包含跨代引用的对象. 

        但是已经生成的卡表, 其中已经尽数标记了内存区域, 但是如果在标记的过程中, 未收集区域的对象持有并修改了它自己的对收集区域的对象的引用, 例如一个对象obj在老年代中, 它本来引用了新生代的 a对象. 但是a对象被标记为回收, 此时obj所在的卡页就会被标记为1(存在跨代引用), 但是如果在扫描的时候, 这个引用被更改, 此时恰好obj所在的卡页没有任何的跨代引用了, 此时卡页的标记应该被置为1, 但是真实情况是不变. 

        接下来我们的问题来了, 我们该怎么阻止对象赋值的那一刻去更新维护卡表? 

        HotSpot通过写屏障来维护卡表状态, 在JVM中,特别是采用分代收集的垃圾收集算法时,写屏障被用于维护卡表(Card Table)的状态。卡表是一种数据结构,用于记录哪些内存区域(称为卡页)包含跨代引用。当一个对象(可能位于老年代)的字段被赋予了一个指向另一个代(如新生代)对象的引用时,写屏障会被触发。这个写屏障会检查并更新卡表,将包含跨代引用的卡页标记为脏。这样,在后续的垃圾收集过程中,垃圾收集器就可以只扫描这些被标记为脏的卡页,而无需扫描整个非收集区域,从而提高垃圾收集的效率



并行的可达性分析

这里的并行指的是用户线程和GC线程

        我们开头讲了可达性分析, 一个对象在做可达性分析的时候, 通过找是否有引用链与这个对象相关联, 来标记一个对象是否已经死亡, 但是如果在数据量过大的时候, 这种扫描就会成为一中性能屏障, 此时你想要穷举每一个对象的引用链, 是很消耗性能的行为, 同时用户线程必须暂停, 防止在运行的过程中修改了引用导致错误的标记

        然后引入了OopMap来减少其寻找时间.

        但是我们后面的几款虚拟机, 其实有提到过, 他们在进行可达性分析的时候一般是标记阶段, 如下: CMS

        可以在标记阶段进行用户线程的执行? 原来不是说用户线程运行的时候, 会修改引用导致错误的标记? 

        想要解决用户线程的停顿, 你就必须对用户线程在GC中做出的引用修改进行一个容错的措施. 例如在进行错误的标记后如何进行补救?  其实我们可以参考Redis这个单线程模型中, 进行RDB全量持久化的时候的策略: 

        首先Redis会fork一个子进程进行内存数据的全量初始化, 但是这个时候, 处理线程其实是没有停止工作的, 这个时候处理线程一边工作, 持久化进程一边将内存中已经有的数据进行持久化, 那么问题来了在工作线程处理的时候, 不断有新的kv写入, 此时该怎么办? 那当然是建立一个新的缓冲区, 用于接收新到来的kv, 然后在子进程持久化玩内存中已经有的数据的时候, 就同步写入缓冲区中的数据

        可达性分析的时候其实也是如此, 对GC时候, 对修改了的引用进行记录, 然后做出补救措施, 重新标记, 这样就保证了数据的一致性. 

        java虚拟机一般有两种方法来解决并发的时候一致性问题: 

  • 增量更新, 也就是上述的方法, 此方法一般用于往GC Root中新增对象引用的情况
  • 原始快照:  适用于删除某个对象的引用的时候, 记录这个被删除的引用, 也就是只要被删除了之后, 就会重新扫描一次.  

         

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/433054.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何把PDF样本册转换为网址链接

​随着互联网的普及,将纸质或PDF格式的样本册转化为网址链接,以便于在线浏览和分享,变得越来越重要。本文将为您详细讲解如何将PDF样本册转换为网址链接,让您轻松实现线上展示和分享。 一、了解PDF样本册与网址链接 1. PDF样本册…

详解电力物联网通常使用哪些通信规约?

在电力物联网行业中,通信规约是关键的技术之一,用于实现电网设备与控制中心之间的数据通信和信息管理。本篇就为大家简单说明电力物联网通常使用哪些通信规约。 1、IEC 60870-5-101/104 这是由国际电工委员会(IEC)制定的一系列标…

99%的人都不知道的AI绘图变现赚钱秘诀,都在这里了!

AI绘画发展至今,已经有很多实际落地的应用场景,这里介绍几种AI绘图热门变现方式 AI儿童绘本 各大平台上故事绘本、幼儿园儿歌、英文绘本、古诗词,从下图里,可以看出需求量很大 AI儿童绘本 实现方式 \1. gpt\2. leonardo.ai\3.…

Arduino的wifi连接,如何关闭低功耗模式?

🏆本文收录于《全栈Bug调优(实战版)》专栏,主要记录项目实战过程中所遇到的Bug或因后果及提供真实有效的解决方案,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&am…

linuxC命令5

目录 2.1概念 2.2格式 5.1根据宏是否定义 5.2根据宏值 5.3防止头文件重复包含 指针函数 2.1概念 本质上是函数,返回值是指针类型 2.2格式 数据类型 * 函数名(参数列表) { 函数体; return 地址;//失败一般会返回NULL } …

虚幻蓝图Ai随机点移动

主要函数: AI MoveTo 想要AI移动必须要有 导航网格体边界体积 (Nav Mesh Bounds Volume) , 放到地上放大 , 然后按P键 , 可以查看范围 然后创建一个character类 这样连上 AI就会随机运动了 为了AI移动更自然 , 取消使用控制器旋转Yaw 取消角色移动组件 的 使用控制器所需的…

AI数字人直播爆火,数字人虚拟主播成品牌闲时直播最佳选择!

近年来,随着互联网的普及和发展,电商和直播平台在我国迅速崛起。根据中国网络信息中心的数据显示,我国直播用户7.5亿,使用率已经超过70%,直播已经成为企业重要的营销和销售通道。 一、在经历了几年的爆发式增长后&…

C++冷门知识点1

1.特殊情况汇总: 负数,空指针,叶节点,INT_MAX和INT_MIN 2.双指针法(快慢指针,头尾指针),三数指针法(链表逆序那块) 3.一定要注意极端情况 2.e后边可以跟负数,但是不能跟小数 3.string的push_bac…

Git 使用方法

简介 Git常用命令 Git 全局设置 获取Git 仓库 方法二用的比较多 将仓库链接复制 在 git base here ----> git clone 仓库链接 工作区、暂存区、版本库 Git 工作区中文件中的状态 本地仓库的操作 远程仓库操作 git pull 将代码推送到远程仓库 1. git add 文件名 ---放…

Visual Studio导出动态库

1、创建新项目,选择如下 2、工程目录结构如下 3、编写pch.h文件,内容如下 // pch.h: 这是预编译标头文件。 // 下方列出的文件仅编译一次,提高了将来生成的生成性能。 // 这还将影响 IntelliSense 性能,包括代码完成和许多代码浏…

实现简易 vuedraggable 的拖拽排序功能

一、案例效果 拖拽计数4实现手动排序 二、案例代码 <draggable:list"searchResult.indicator":group"{ name: indicators }"item-key"field"handle".drag-handle-icon"><divclass"field-item"v-for"(item…

JAVA一键预约品质生活尽在掌握高效家政服务系统小程序源码

一键预约&#xff0c;品质生活尽在掌握 —— 高效家政服务系统 &#x1f3e0;【开篇&#xff1a;告别繁琐&#xff0c;拥抱品质生活】&#x1f3e0; 在这个快节奏的时代&#xff0c;我们总在为生活奔波&#xff0c;却往往忽略了家的温馨与整洁。你是否也曾为堆积如山的家务而烦…

禁止吸烟监测系统 基于图像处理的吸烟检测系统 YOLOv7

吸烟是引发火灾的重要原因之一。烟头在未熄灭的情况下&#xff0c;其表面温度可达200℃-300℃&#xff0c;中心温度甚至能高达700℃-800℃。在易燃、易爆的生产环境中&#xff0c;如化工厂、加油站、仓库等&#xff0c;一个小小的烟头就可能引发灾难性的火灾&#xff0c;造成巨…

HarmonyOS---权限和http/Axios网络请求

网络请求(http,axios) 目录 一、应用权限管理1.1权限的等级1.2授权方式1.3声明权限的配置1.4如何向用户进行申请 二、内置http请求使用三、Axios请求使用&#xff08;建议&#xff09;3.1 使用方式一3.2 使用方式二&#xff08;建议&#xff09; 一、应用权限管理 应用权限保护…

【Linux】环境变量(初步认识环境变量)

文章目录 1. 环境变量1.1 基本概念 2. 认识常见环境变量2.1 PATH2.2 HOME2.3 SHELL2.4 PWD2.5 USER 3. 理解环境变量 1. 环境变量 在main函数的命令行参数中&#xff0c;有argc、argv、env三个参数。 argc&#xff1a;命令函参数的个数argc&#xff1a;存放每个参数的具体数值…

54 循环神经网络RNN_by《李沐:动手学深度学习v2》pytorch版

系列文章目录 文章目录 系列文章目录循环神经网络使用循环神经网络的语言模型困惑度&#xff08;perplexity&#xff09;梯度剪裁 循环神经网络 使用循环神经网络的语言模型 输入“你”&#xff0c;更新隐变量&#xff0c;输出“好”。 困惑度&#xff08;perplexity&#xff…

vue中高德地图使用 Marker 标点 - 标点数据快到 1000 时页面卡顿问题解决(已解决 - 多方面原因)+ 海量点功能实现解决

目录 1.业务需求2.最初实现及出现的问题3.解决 - 1000 个标点时页面就出现 卡顿 问题4.使用海量点、聚合标点后还有卡顿&#xff0c;排查其他原因5.最终解决5.1页面中list数据渲染问题解决5.2地图相关实例不要放在 vue 的可响应数据中 页面展示 1.业务需求 需要在 高德地图 中标…

LLM - 理解 多模态大语言模型(MLLM) 的 幻觉(Hallucination) 与相关技术 (七)

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/142463789 免责声明&#xff1a;本文来源于个人知识与公开资料&#xff0c;仅用于学术交流&#xff0c;欢迎讨论&#xff0c;不支持转载。 多模态…

使用session来实现验证码发送功能

一、什么是session&#xff1f; Session由服务器创建&#xff0c;并为每一次会话分配一个Session对象。同一个浏览器发起的多次请求&#xff0c;同属于一次会话&#xff08;Session&#xff09;。首次使用到Session时&#xff0c;服务器会自动创建Session&#xff0c;并创建Co…

【HarmonyOS】鸿蒙多Toast显示工具类

【HarmonyOS】封装可以同时显示多个toast的工具类 src/main/ets/common/MyPromptActionUtil.ets import { ComponentContent, PromptAction, window } from kit.ArkUI; import { BusinessError } from kit.BasicServicesKit;// MyPromptInfo 类用于生成唯一的 dialogID export …