JVM系列 | 对象的消亡——HotSpot的设计细节

news2025/4/7 20:21:50

HotSpot 的细节实现

文章目录

  • HotSpot 的细节实现
    • OopMap 与 根节点枚举
      • 根节点类型及说明
      • HotSpot中的实现
    • OopMap 与 安全点
      • 安全点介绍
      • 如何保证程序在安全点上?
    • 安全区域
    • 记忆集与卡表
      • 记忆集
      • 卡表
    • 写屏障
    • 并发的可达性分析(与用户线程)
      • 并发可达性分析 存在的问题
      • 解决方案

笔者寄语:这一篇博客真的写的脑壳痛,很多地方太抽象了,想要仔细的扣细节很费脑袋,本来早就想发布了,但是因为要扣的点太多了,一直拖到了现在。

OopMap 与 根节点枚举

无论是标记复制算法、标记整理算法、标记清除算法,都需要有一个从“根节点”开始的可达性分析工序,因此确定出哪些节点是根节点就很重要。

根节点类型及说明

  1. 全局静态变量(Global Static Variables)
  • 共享和生命周期长:全局静态变量在整个程序生命周期内都是存在的,它们在程序的任何部分都可以被访问。
  • 可达性:因为全局静态变量可以引用任何对象,并且这些对象可能被多个线程使用,所以它们必须作为根节点来追踪。
  1. 栈帧中的局部变量(Local Variables in Stack Frames)
  • 活动性:这些是当前正在执行的方法或函数中的局部变量。因为它们正在被使用,所以引用的对象不能被回收。
    可达性:这些局部变量在栈帧中存储,GC需要从这些变量开始追踪,以确保它们引用的对象不会被回收。
  1. 当前使用中的线程(Currently Active Threads)
  • 线程对象:每个活动的线程对象都会被作为根节点,因为线程本身以及它们的栈帧包含的局部变量和执行上下文需要被追踪。
  1. JNI引用(JNI References)
  • 本地方法接口:在使用Java本地接口(JNI)调用本地代码时,本地代码可以创建引用指向Java对象,这些引用也必须作为根节点来追踪,以确保这些Java对象在本地代码使用期间不会被回收。
  1. 类加载器(Class Loaders)
  • 类和静态变量的管理:类加载器是负责加载Java类的对象,它们持有对类和静态变量的引用,因此它们的引用链必须被追踪,以确保这些类和静态变量不会被错误地回收。
  1. 系统类(System Classes)
  • 核心类库:Java中的一些核心类库,例如java.lang.System、java.lang.Thread等,持有对大量静态变量和对象的引用,这些系统类也必须作为根节点来追踪。
  1. GC Roots in Java Heap (某些Java堆中的对象)
  • 常量池(String Pool):常量池中的字符串对象,以及一些其他常量值会被作为根节点来追踪。
  • 类静态字段(Static Fields of Classes):静态字段属于类的对象,它们在类加载时被创建,在类卸载时被回收。

HotSpot中的实现

在HotSpot中进行垃圾回收的第一个步骤就是“根节点枚举”,根节点枚举的过程必须要停止用户线程(也就是停止代码程序),这一步骤被称为"Stop The World"(时停/酷);且现在程序一般都非常的大,堆等内存动辄几百上千M,如果要一个个对象扫描来判断类型的话耗时实在是可观,因此这种操作肯定是不可取的。

HotSpot为了解决这一问题,引入了OopMap:一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。

如何理解偏移量呢?就是与这个类的起始地址的差,也就是从开头向后数多少个地址是一个什么类型的数据。

这样一来,JVM就无需扫描整个运行时内存空间,而是直接扫描OopMap就可以获得当前运行时内存有哪些根节点,从而解约根节点枚举时的时间。

OopMap 与 安全点

安全点介绍

《深入理解Java虚拟机:原文》可能导致引用关系变化,或者说导致OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外存储空间,这样垃圾收集伴随而来的空间成本就会变得无法忍受的高昂。

可见,引如OopMap并无问题,我们还需要解决什么时候更新OopMap的问题:过多的安全点会导致性能开销过大,而过少的安全点会导致垃圾收集或其他全局操作无法及时执行…经过JVM团队深思熟虑,终于给出了解决方案:在所有线程进入某些特定代码位置之后更新OopMap(通常更新完成之后直接进行垃圾收集操作),这些特定位置就被称为安全点

安全点通常是:

  1. 循环的回边(一次循环结束后 回到循环开始的位置 这两者之间)

  2. 方法的开始与结束

  3. 抛异常的位置

下面用代码进行一个简单的实力

public class GCExample {
    private static Object staticObject = new Object();

    public static void main(String[] args) {
        Object localObject = new Object();
        
        // 模拟一个循环,其中可能触发安全点
        for (int i = 0; i < 1000000; i++) {
            // 模拟方法调用和循环迭代
            someMethod(localObject);
        }

        // 主线程请求垃圾收集
        System.gc();
    }

    private static void someMethod(Object obj) {
        // 模拟方法调用,可能插入安全点
        System.out.println("Processing: " + obj);
    }
}

如何保证程序在安全点上?

要在进行垃圾回收的时候,保证所有的线程都停留在安全点上,有两种方式:

  1. 抢占式
  2. 主动式

抢先式中断不需要线程的执行代码主动去配合,在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程响应GC事件。

主动式中断的思想是当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的,另外还要加上所有创建对象和其他需要在Java堆上分配内存的地方,这是为了检查是否即将要发生垃圾收集,避免没有足够内存分配新对象。

由于轮询操作在代码中会频繁出现,这要求它必须足够高效。HotSpot使用内存保护陷阱的方式(自陷陷阱),关于自陷陷阱这里不多赘述,可以自己搜一下。

安全区域

针对一些无法进入安全点的线程(如Sleep或Blocked的线程),虚拟机设置了安全区域来应对。

安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。

记忆集与卡表

记忆集

万事俱备,接下来似乎要进行垃圾回收了,这里又有一个问题,如果存在跨代引用怎么办?

跨代引用是指老年代中的对象引用年轻代中的对象,或者反过来年轻代引用老年代。由于垃圾收集是分代进行操作的,老年代的收集频率远低于年轻代,那么在对年轻代进行垃圾收集的时候,如果一个老年代的对象O引用了年轻代对象Y,而JVM并不知道这一点,在扫描完整个年轻代之后发现没有对象引用对象Y,这时候将Y给销毁了,就会造成程序运行错误。

如何解决这一点问题?JVM给出的答案是:采用一块专门的内存区域用于记录哪一些对象发生了跨代引用,这便是记忆集

比如如果老年代O引用了年轻代的Y,那么就在记忆集中记录下O对Y的引用,这样在对年轻代进行垃圾收集的时候(我是指标记阶段),就能顺便给O也带进来标记一下,这样便能标记到Y。反之亦是如此。

卡表

但是JVM团队还是觉得这样的记录太过于浪费内存空间,如果存在大量的跨代引用的话,使用的内存空间确实十分可观,因此JVM团队决定放大粒度,一般来说,存在以下三个精度:

  1. 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的32位或64位,这个精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针

  2. 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针

  3. 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针

卡精度就是卡表,使用卡精度,JVM将不再记录内存或对象,而是记录一个大致的范围,在这个内存范围内存在跨代引用,在进行垃圾回收的时候,只需要额外将这一块内存区域也扫描进去即可。

在这里插入图片描述

比如这张图中,由于O7所在的内存区域与O20所在的内存区域存在对新生代的跨带引用,因此对新生代进行垃圾回收的时候,也会将上图两个红色的区域算进去。


*在一些新的垃圾回收器中不存在年轻代与老年代,而是使用了更复杂的区域收集,也会涉及跨带引用

写屏障

写屏障是十分好理解的,在这里就引用一些原文内容了,简单来说就是指令层面的AOP操作。

写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴内。在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier),在赋值后的则叫作写后屏障(Post-Write Barrier)。HotSpot虚拟机的许多收集器中都有使用到写屏障,但直至G1收集器出现之前,其他收集器都只用到了写后屏障。

应用写屏障后,虚拟机就会为所有赋值操作生成相应的指令,一旦收集器在写屏障中增加了更新卡表操作,无论更新的是不是老年代对新生代对象的引用,每次只要对引用进行更新,就会产生额外的开销,不过这个开销与Minor GC时扫描整个老年代的代价相比还是低得多的。


除了写屏障的开销外,卡表在高并发场景下还面临着“伪共享”(False Sharing)问题。伪共享是处理并发底层细节时一种经常需要考虑的问题,现代中央处理器的缓存系统中是以缓存行(Cache Line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低,这就是伪共享问题。

假设处理器的缓存行大小为64字节,由于一个卡表元素占1个字节,64个卡表元素将共享同一个缓存行。这64个卡表元素对应的卡页总的内存为32KB(64×512字节),也就是说如果不同线程更新的对象正好处于这32KB的内存区域内,就会导致更新卡表时正好写入同一个缓存行而影响性能。为了避免伪共享问题,一种简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元素未被标记过时才将其标记为变脏。

并发的可达性分析(与用户线程)

解决了根节点枚举、安全点、记忆集、写屏障之后,终于可以进行可达性分析了。

在最初的垃圾收集器中(如Serial、ParNew收集器),在进行可达性分析的时候都需要暂停用户线程,直到CMS收集器的出现,才终于实现了用户线程与垃圾收集线程共同工作的场景。

小故事:在最初始版本的垃圾收集器中,JVM没执行一段时间都需要Stop The World(时停)进行垃圾收集工作,那时候Java是十分“慢”的,而JVM的开发者们也很委屈:你总不能在你妈妈收拾房间的时候还要乱丢垃圾吧,这样什么时候能打扰完?在你妈妈收拾房间的时候你就应该乖乖坐在沙发上。

并发可达性分析 存在的问题

为了让垃圾回收更快!!就必须要让垃圾回收线程与用户线程同时执行!!但若是用户线程与垃圾收集线程同步进行,那么就可能造成已经扫描过的确定被引用的对象不再被引用了,或者已经扫描过的确定不再被引用的对象又被引用了;这两种情况。这就是我们本章节要弄懂的问题。

为了更好说明对象的引用关系,这里采用三色标记法进行说明:

  1. 黑色:已经被标记是被引用的对象
  2. 白色:未扫描到的对象
  3. 灰色:已经确认被引用,但是还没有扫描它的引用关系的对象

整个图就像是一个波纹,黑色在波纹内,白色在波纹外,灰色是波峰…

在这里插入图片描述

上图前三张图片说明了用户线程在静止状态下的扫描过程;后两张图说明了用户线程与垃圾回收线程同时工作时可能会造成的情况。


注意,如果垃圾回收线程与用户线程并行操作,那么会出现两种情况:

  1. 已经被标记为黑色(被引用)的对象突然不再引用了(浮动垃圾)
  2. 已经被标记为白色(未被引用)的对象突然被引用了(危险!!)

如果是情况1的话,这些对象不被清理问题不大,顶多是占用一些内存,产生一些“浮动垃圾”,但如果是情况2对象小时,那么就很危险了,原本存在引用关系的对象被清除了,那么会造成致命错误,可能会导致程序崩溃。

解决方案

为了解决这一问题,1994年Wilson在理论上证明了,当满足两个条件的时候,才会产生对象消失问题:

  1. 赋值器插入了一条或多条从黑色对象到白色对象的新引用
  2. 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用

这两句话理解起来十分拗口,也就是说,新插入了一个由黑色对象对白色对象的引用关系,且该白色对象(原本就)无法被扫描到(原本就不可达或删除了其可达关系)。

只需要破坏上面的任意一个条件,就不会造成“消失对象”问题。

方法一:增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。

方法二:原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。

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

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

相关文章

计算机底层知识点(一)晶体管与CPU执行指令之间的联系

该文主要通过详细介绍晶体管在CPU执行指令时的作用。本文所讲解例子为NPN型二极管。这里简单介绍一下&#xff0c;NPN是共阳极&#xff0c;即两个NP结的P结相连为基极&#xff0c;另两个N结分别做集电极和发射极&#xff0c;发射极电流 集电极电流 基极电流。 图5 LED两脚分别…

【Vue3】具名插槽

【Vue3】具名插槽 背景简介开发环境开发步骤及源码 背景 随着年龄的增长&#xff0c;很多曾经烂熟于心的技术原理已被岁月摩擦得愈发模糊起来&#xff0c;技术出身的人总是很难放下一些执念&#xff0c;遂将这些知识整理成文&#xff0c;以纪念曾经努力学习奋斗的日子。本文内…

纯技术手段实现内网穿透,免注册免收费

纯技术手段实现内网穿透&#xff0c;免注册免收费 一、内网穿透二、方法分类2.1 基于隧道协议的内网穿透2.2 基于反向代理的内网穿透2.3 基于SSH的内网穿透具体工具的分类如下&#xff1a;基于隧道协议基于反向代理基于SSH 三、本文方法四、具体操作4.1 安装服务端4.2 安装客户…

【Linux 网络】链路层

文章目录 链路层1 以太网1.1 以太网帧格式1.2 MAC地址1.3 MTU 2. ARP协议2.1 ARP协议的作用2.2 ARP数据报格式2.3 ARP的流程 其他协议3. DNS协议3.1 域名3.2 输入URL后的事情 4. ICMP协议4.1 ICMP 功能都有啥&#xff1f;4.2 基于ICMP的命令ping命令 5. NAT协议5.1 NAT技术背景…

android13关机按钮 去掉长按事件 去掉启动到安全模式 删除关机长按

总纲 android13 rom 开发总纲说明 目录 1.前言 2.界面效果 3.问题分析 4.代码修改 5.编译替换运行 6.彩蛋 1.前言 在Android操作系统中,关机按钮通常具有多种功能,包括短按关机、长按启动语音助手或重启设备等。在某些情况下,用户或设备管理员可能希望自定义关机按…

爬虫中常见的加密算法Base64伪加密,MD5加密【DES/AES/RSA/SHA/HMAC】及其代码实现(一)

目录 基础常识 Base64伪加密 python代码实现 摘要算法 1. MD5 1.1 JavaScript 实现 1.2 Python 实现 2. SHA 2.1 JavaScript 实现 2.2 Python 实现 2.3 sha系列特征 3. HMAC 3.1 JavaScript 实现 3.2 Python 实现 对称加密 一. 常见算法归纳 1. 工作模式归纳 …

码农职场:一本专为IT行业求职者量身定制的指南

目录 写在前面 推荐图书 推荐理由 写在后面 写在前面 本期博主给大家推荐一本专为IT行业求职者量身定制的指南&#xff1a;《码农职场》。 推荐图书 https://item.jd.com/14716160.html 内容简介 这是一本专为广大IT 行业求职者量身定制的指南&#xff0c;提供了从职前…

使用Python实现栅格划分(渔网)

在QGIS中&#xff0c;“渔网”&#xff08;Fishnet&#xff09;是指一种创建规则网格&#xff08;通常是矩形或正方形&#xff09;的工具&#xff0c;这些网格可以用于空间数据的采样、分区或作为其他地理空间分析的基础。渔网工具可以生成一个由多边形组成的图层&#xff0c;每…

文件解析漏洞—IIS解析漏洞—IIS7.X

在IIS7.0和IIS7.5版本下也存在解析漏洞&#xff0c;在默认Fast-CGI开启状况下&#xff0c;在一个文件路径/xx.jpg后面加上/xx.php会将 “/xx.jpg/xx.php” 解析为 php 文件 利用条件 php.ini里的cgi.fix_pathinfo1 开启IIS7在Fast-CGI运行模式下 在 phpstudy2018 根目录创建…

4、postgresql拓展表空间

base是数据保存目录&#xff0c; OID&#xff1a;对象标识符&#xff0c;无符号4字节整数&#xff0c; 数据库的oid在pg_database中&#xff0c;表&#xff0c;索引&#xff0c;序列等OID存储在pg_class中 表空间&#xff1a;pg最大的逻辑存储单元&#xff0c;表索引数据库都…

Linux安装Zabbix7.0并且使用外置Mysql数据库

MySQL 数据库服务版本。必须至少为 8.00.30 # rpm -Uvh https://repo.zabbix.com/zabbix/7.0/alma/9/x86_64/zabbix-release-7.0-5.el9.noarch.rpm # dnf clean all #安装zabbix # dnf install zabbix-server-mysql zabbix-web-mysql zabbix-nginx-conf zabbix-sql-scripts za…

【一图学技术】6.反向代理 vs API网关 vs 负载均衡的原理和使用场景

反向代理 vs API网关 vs 负载均衡 一、概念 ​ &#x1f30f;反向代理&#xff08;Reverse Proxy&#xff09;是一种位于服务器和客户端之间的代理服务器。 ​ 它接收来自客户端的请求&#xff0c;并将其转发给后端服务器&#xff0c;然后将后端服务器的响应返回给客户端。客…

dfs,CF 196B - Infinite Maze

一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 https://codeforces.com/problemset/problem/196/B 二、解题报告 1、思路分析 考虑如何判断一条路径可以无限走&#xff1f; 我们对朴素的网格dfs改进&#xff0c;改进为可以dfs网格外的区域 如果存在某个…

Go语言加Vue3零基础入门全栈班10 Go语言+gRPC用户微服务项目实战 2024年07月31日 课程笔记

概述 如果您没有Golang的基础&#xff0c;应该学习如下前置课程。 Golang零基础入门Golang面向对象编程Go Web 基础Go语言开发REST API接口_20240728Go语言操作MySQL开发用户管理系统API教程_20240729Redis零基础快速入门_20231227GoRedis开发用户管理系统API实战_20240730Mo…

大模型下的视频理解video understanding

数据集 Learning Video Context as Interleaved Multimodal Sequences Motivation&#xff1a; 针对Narrative videos, like movie clips, TV series, etc.&#xff1a;因为比较复杂 most top-performing video perception models 都是研究那种原子动作or人or物 understandin…

C++ 布隆过滤器

1. 布隆过滤器提出 我们在使用新闻客户端看新闻时&#xff0c;它会给我们不停地推荐新的内容&#xff0c;它每次推荐时要去重&#xff0c;去掉 那些已经看过的内容。问题来了&#xff0c;新闻客户端推荐系统如何实现推送去重的&#xff1f; 用服务器记录了用 户看过的所有历史…

OpenStack——存储服务

存储侧&#xff1a; 块存储 文件存储 对象存储 存储简介 特点&#xff1a; 1、OS盘只能使用块存储 2、不能实现共享【不能解决两个主机同时去读写同一个block的问题】 3、性能最优 filesystem——文件存储 VIMS&#xff1a;高可用文件系统 ——提供了锁机制 对象存储 ——解…

MySQL搭建主从复制和读写分离(数据库管理与高可用)

集群&#xff1a; 高可用&#xff1b; 负载均衡&#xff1b; 高性能 1、MySQL主库在事务提交时把数据变更&#xff08;insert、delet、update&#xff09;作为事件日志记录在二进制日志表&#xff08;binlog&#xff09;里面。 2、主库上有一个工作线程 binlog dump thread…

蓝桥杯 DNA序列修正

今天再刷蓝桥的题目时&#xff0c;发现这道题目的第二种更为简洁的做法&#xff1b; 首先题目描述如下&#xff1a; 样例输入 5 ACGTG ACGTC 样例输出 2 对于这道题目&#xff0c;我们想的是用两个数组将其分别存储下来&#xff0c;然后再根据A-T、G-C的配对关系将数组二&a…

【C语言】堆排序

堆排序即利用堆的思想来进行排序&#xff0c;总共分为两个步骤&#xff1a; 1. 建堆 升序&#xff1a;建大堆 降序&#xff1a;建小堆 原因分析&#xff1a; 若升序建小堆时间复杂度是O(N^2) 升序建大堆&#xff0c;时间复杂度O&#xff08;N*logN&#xff09; 所以升序建大堆…