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

news2024/12/26 22:24:15

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/1984768.html

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

相关文章

Spring boot框架指南

1. Spring Boot 概述 1.1 定义与起源 Spring Boot是一种基于Spring框架的开源框架&#xff0c;旨在简化Spring应用程序的创建和开发过程。它通过提供一系列默认配置和自动配置功能&#xff0c;减少了开发者在配置上的工作量&#xff0c;使得快速搭建生产级别的Spring应用程序…

OV SSL证书优势及获取渠道

OV证书&#xff0c;即组织验证型SSL证书&#xff0c;通过严格的组织审查流程&#xff0c;为网站提供数据传输加密、身份验证和信息完整性保护。 OV证书优势 1 高信任度 OV证书通过证书颁发机构&#xff08;CA&#xff09;对企业实名认证&#xff0c;包括企业名称、注册地址、…

万能门店小程序开发平台功能源码系统 带完整的安装代码包以及安装搭建教程

互联网技术的迅猛发展和用户对于便捷性需求的不断提高&#xff0c;小程序以其轻量、快捷、无需安装的特点&#xff0c;成为了众多商家和开发者关注的焦点。为满足广大商家对于门店线上化、智能化管理的需求&#xff0c;小编给大家分享一款“万能门店小程序开发平台功能源码系统…

kernel32.dll丢失?那么kernel32.dll如何修复?教你几种修复丢失kernel32.dll错误的方法

在使用电脑时你是否遇到过kernel32.dll丢失的情况&#xff0c;那么遇到这种情况应该如何解决呢&#xff1f;遇到kernel32.dll丢失就会导致电脑无法正常运行&#xff0c;应用程序也会无法正常使用&#xff0c;今天就教大家kernel32.dll丢失的解决办法。 几种解决kernel32.dll丢失…

破解USB设备通讯协议实现自定义软件控制的步骤与方法

在设备和计算机之间通过USB进行通讯的情况下&#xff0c;厂家提供的软件可以控制设备&#xff0c;但没有提供任何其他资料和支持&#xff0c;这种情况下&#xff0c;若希望自行开发软件来实现同样的功能&#xff0c;可以通过以下步骤破解通讯协议并开发自定义程序。 1. 捕获US…

干货!如何选择Ai大模型(LLMs)?

过去一年里&#xff0c;大型语言模型&#xff08;LLMs&#xff09;在人工智能界风起云涌&#xff0c;纷纷以突破性的进步拓展生成式人工智能的可能性。新模型层出不穷&#xff0c;令人目不暇接。 这些模型依靠日益增长的参数数量和庞大的数据集进行训练&#xff0c;显著提升了…

CentOS版本的Linux系统误删了自带的python和yum,恢复过程

文章借鉴于&#xff1a;Centos误删自带python2.7恢复方法_centos默认的被卸载了-CSDN博客 在进行别的操作的时候&#xff0c;一不小心将我的系统自带的Python2.7.5和yum删除掉了。 后来我尝试重新安装yum&#xff0c;但是安装yum必须要有python。 我又去重新安装了python&am…

前端面试:项目细节重难点问题分享(16)

&#x1f481;&#x1f481;更多详情&#xff1a;爱米的前端小笔记&#xff08;csdn~xitujuejin~zhiHu~Baidu~小红shu&#xff09;同步更新&#xff0c;等你来看&#xff01;都是利用下班时间整理的&#xff0c;整理不易&#xff0c;大家多多&#x1f44d;&#x1f49b;➕&…

数据库|SQLServer数据库:数据的基本查询

哈喽&#xff0c;你好啊&#xff0c;我是雷工&#xff01; 其实对于数据库的查询以前也应用过&#xff0c;只是使用时一般也是根据要实现的要求从网上搜索&#xff0c;能实现应用要求即可&#xff0c;现在根据视频学习&#xff0c;正好有这么一节&#xff0c;就听一遍&#xff…

ROS 2 话题:使用C++进行发布与订阅

ROS 2 话题&#xff1a;使用C进行发布与订阅 ROS 2提供了一种强大而灵活的通信机制&#xff0c;称为话题&#xff08;Topics&#xff09;。话题允许节点之间进行异步消息传递&#xff0c;从而实现松耦合的通信模式。在本文中&#xff0c;我们将介绍如何使用C编程语言在ROS 2中…

常见中间件漏洞大全及其修复方法(未完成)

一.Tomcat tomcat 是一个开源而且免费的 jsp 服务器&#xff0c;默认端口 : 8080 &#xff0c;属于轻量级应用服务器。它可以实现 JavaWeb程序的装载&#xff0c;是配置 JSP &#xff08; Java Server Page &#xff09;和 JAVA 系统必备的一款环境。 1.1 CVE-2017-12615 Tomc…

Transformers 中的Softmax 和 Layer Norm 如何并行?

1.Softmax 如何并行&#xff1f; Softmax 计算公式&#xff1a; 安全的 Softmax 运算&#xff1a; softmax 有个问题&#xff0c;那就是很容易溢出。比如采用半精度&#xff0c;由于float16的最大值为65504&#xff0c;所以只要x>11&#xff0c;那么softmax就溢出了。即使是…

C语言新手小白详细教程(6)函数

希望文章能够给到初学的你一些启发&#xff5e; 如果觉得文章对你有帮助的话&#xff0c;点赞 关注 收藏支持一下笔者吧&#xff5e; 阅读指南&#xff1a; 开篇说明为什么要使用函数&#xff1f;1.定义一个函数2.调用函数3.定义函数详解 开篇说明 截止目前&#xff0c;我们已…

华清IOday7 24-8-5

文章目录 使用有名管道实现&#xff0c;一个进程用于给另一个进程发消息&#xff0c;另一个进程收到消息后&#xff0c;展示到终端上&#xff0c;并且将消息保存到文件上一份使用有名管道实现两个进程间相互通信 使用有名管道实现&#xff0c;一个进程用于给另一个进程发消息&a…

服务器数据恢复—raid5阵列上层Oracle数据库数据恢复案例

服务器数据恢复环境&故障&#xff1a; 一台服务器上有8块SAS硬盘&#xff0c;其中的7块硬盘组建了一组RAID5阵列&#xff0c;另外1块硬盘作为热备盘使用。划分了6个LUN&#xff0c;服务器上部署有oracle数据库。 RAID5磁盘阵列中有2块硬盘出现故障并离线&#xff0c;RAID5阵…

浮点数在计算机中的编码方式

一、前言 我们常能听到&#xff0c;直接用浮点数做运算得出的结果是不准确的了&#xff1b;或者也能看到涉及到浮点数时&#xff0c;会出现一些奇奇怪怪的问题&#xff0c;比如&#xff1a; public class DecimalTest {public static void main(String[] args) {float f1 1.…

STK12.2+Python开发(二):添加访问约束,新建场景、卫星、地面站等,获取当前场景的信息

新建场景 1.获取当前打开的场景 #获取当前打开的场景 scenario root.CurrentScenario2.设置当前场景的时间 scenario.SetTimePeriod(Today,24hr)3.添加一个地面目标到当前的场景 scenario.SetTimePeriod(Today,24hr)4.添加一个地面目标到当前的场景&#xff0c;括号内是三…

空气质量传感器 - 从零开始认识各种传感器【二十八期】

空气质量传感器|从零开始认识各种传感器 1、什么是空气质量传感器&#xff1f; 空气质量传感器是一种检测空气中污染物浓度的设备&#xff0c;广泛应用于环境监测、智能家居、工业控制和健康管理等领域。 2、空气质量传感器是如何工作的&#xff1f; 对于每个人都关心的空气质…

效率何止10倍!利用输入法瞬间调用提示词

我们在日常工作/学习/生活有很多场景需要使用提示词&#xff0c;比如说&#xff1a; 快速总结文章快速排版解释概念翻译其它经常面对的任务 但是使用提示词有几个痛点&#xff1a; 你很难临时写一个非常完整的提示词你凑合写的提示词&#xff0c;又担心结果不满意如果已经保…

前端使用css动画绘制简易的进度条,数据多条的时候可以切换

文章目录 一、效果图二、使用步骤1.公共的进度条组件2.使用 总结 一、效果图 二、使用步骤 1.公共的进度条组件 我这里命名的progressBar.vue&#xff0c; 你们使用的时候直接复制粘贴到自己的项目里面即可。 文件中代码如下&#xff08;示例&#xff09;&#xff1a; <t…