JVM 垃圾回收算法细节

news2024/12/26 13:27:40

目录

前言

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.coloradmin.cn/o/2180433.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

聊一下数据脱敏

背景 随着信息社会高速发展,大家对隐私数据的安全越来越重视,现在市面上各种搜集个人信息的网站,app层出不穷,你也不知道你的信息提交上去后,提供服务的那边,是不是会妥善保管好你的个人数据,包…

Python和MATLAB库尔巴克–莱布勒散度信息论统计学生物学和算法模型

🎯要点 高斯混合模型聚类和t分布随机邻域嵌入底层分析信息论测量复合彩票统计学计算结果离散分布速率最优估计器样本统计相似性快速闭环散度和交叉熵计算催乳素诱导模型贝叶斯快速推理模型视觉皮层活动神经数据分布 Python散度 在数理统计中,库尔巴克…

悬浮提词器免费版,5款便捷软件分享推荐

在这个信息爆炸、内容为王的时代,无论是直播带货、视频创作还是公开演讲,流畅自然的表达都是吸引观众的关键。然而,面对镜头时忘词卡顿却成了不少人的“心头痛”。今天,就给大家揭秘五款完全免费的悬浮提词器软件,它们…

初学51单片机之I2C总线与E2PROM二

总结下上篇博文的结论: 1:ACK信号在SCL为高电平期间会一直保持。 2:在字节数据传输过程中如果发送电平跳变,那么电平信号就会变成重复起始或者结束的信号。(上篇博文的测试方法还是不能够明确证明这个结论&#xff0…

GWAS分析中显著位点如何注释基因:excel???

大家好,我是邓飞。 今天星球的小伙伴问了一个问题: 我现在在做GWAS分析,现在已经找到性状关联的SNP位点,下一步我如何根据position 找到基因呢? 关于基因注释,之前写过一些博客,可以用到的软件…

mono.cecil实现动态插入IL代码

准备 namespace ConsoleApp1 {internal class Program{static void Main(string[] args){Console.WriteLine("Hello, World!");Console.ReadKey();}} }编译出来如下内容 使用ilspy打开 代码 安装Mono.Cecil包 代码如下 using Mono.Cecil; using Mono.Cecil.Ci…

云计算Openstack Neutron

OpenStack Neutron是OpenStack云计算平台中的网络服务组件,它为OpenStack提供了强大的网络连接功能。 一、基本概念 Neutron是一个网络服务项目,旨在为OpenStack提供网络连接。它允许用户创建和管理虚拟网络,包括子网、路由、安全组等&…

Arweave的出块原理

一、Arweave 关键技术 1.1数据结构:Blockweave 区块纺 区块坊中包括区块哈希列表和钱包列表。拥有区块哈希列表使旧区块可以被请求/验证,拥有钱包列表可以验证新交易,而无需处理钱包上一次交易所在的区块。区块哈希列表和钱包列表由矿工保持…

【最新华为OD机试E卷-支持在线评测】字符串分割转换(100分)多语言题解-(Python/C/JavaScript/Java/Cpp)

🍭 大家好这里是春秋招笔试突围 ,一枚热爱算法的程序员 💻 ACM金牌🏅️团队 | 大厂实习经历 | 多年算法竞赛经历 ✨ 本系列打算持续跟新华为OD-E/D卷的多语言AC题解 🧩 大部分包含 Python / C / Javascript / Java / Cpp 多语言代码 👏 感谢大家的订阅➕ 和 喜欢�…

基于大数据的电脑硬件推荐及数据分析系统

作者:计算机学姐 开发技术:SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等,“文末源码”。 专栏推荐:前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏:…

基于卷积神经网络的体育运动项目分类识别系统

温馨提示:文末有 CSDN 平台官方提供的学长 QQ 名片 :) 1. 项目简介 随着计算机视觉和深度学习技术的快速发展,利用先进的图像处理技术对体育运动进行智能分类与识别已成为研究热点。传统的运动分析方法通常依赖于人工观察和记录,耗时耗力且容…

fatfs API使用手册

配置 /*---------------------------------------------------------------------------/ / Configurations of FatFs Module /---------------------------------------------------------------------------*/#define FFCONF_DEF 80286 /* Revision ID *//*---------------…

JWT(JSON Web Token)的介绍

JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案,本文介绍它的原理和用法。 一、跨域认证的问题 互联网服务离不开用户认证。一般流程是下面这样。 1、用户向服务器发送用户名和密码。 2、服务器验证通过后,在当前对话&…

如何组织鼠标的默认的事件

如何组织鼠标的默认的事件 我原先的代码是 dblclick"checkNode(data)"设置了一个双击的事件,我如果双击的话就会导致这个内容被选中。 选中内容的同时会触发浏览器默认的操作,导致出现复制的框这些东西。 解决的方法。加一句。 mousedown.pr…

尝鲜使用 YOLO V11 Fine-Tuning 训练自定义的目标检测模型

一、YOLO V11 2024年9月30日,Ultralytics官方团队宣布YOLOv11正式发布,标志着YOLO系列实时目标检测器的又一次重大升级。这一新版本不仅在准确性和检测速度上再创新高,还通过架构和训练方法的革新,极大地提升了目标检测的综合性能…

算法打卡:第十一章 图论part11

今日收获:Floyd 算法,A * 算法,最短路算法总结 1. Floyd 算法 题目链接:97. 小明逛公园 思路:Floyd用于解决多源最短路问题,对边的正负权值没有要求。核心是动态规划 (1)dp数组的…

Stable Diffusion的Lora使用和训练 如何使用和训练LoRA模型?你想要的都在这!--人人都可以当炼金术士!

随着人工智能技术的不断发展,图像生成与反推技术已经成为了AI领域的一大热点。今天,我们就来为大家详细介绍Stable Diffusion的Lora使用和训练方法,让每个人都能成为炼金术士,创造出属于自己的图像生成魔法! 在我们使…

108.WEB渗透测试-信息收集-FOFA语法(8)

免责声明:内容仅供学习参考,请合法利用知识,禁止进行违法犯罪活动! 内容参考于: 易锦网校会员专享课 上一个内容:107.WEB渗透测试-信息收集-FOFA语法(7) 指挥系统的后台&#xff…

把GV幅度还不赶紧地方吧

文章目录 标题1代码 标题1 代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</titl…

pve虚拟机常见问题汇总

一、问题记录 当你遇到 Proxmox VE (PVE) 报错信息 “TASK ERROR: can’t lock file ‘/var/lock/qemu-server/lock-141.conf’ - got timeout”&#xff0c;通常意味着 PVE 尝试对一个虚拟机进行操作&#xff08;如停止或重启&#xff09;时&#xff0c;无法获取必要的锁文件&…