JVM:ZGC详解(染色指针,内存管理,算法流程,分代ZGC)

1,ZGC(JDK21之前)

ZGC 的核心是一个并发垃圾收集器,所有繁重的工作都在Java 线程继续执行的同时完成。这极大地降低了垃圾收集对应用程序响应时间的影响。

  • ZGC为了支持太字节(TB)级内存,设计了基于页面(page)的分页管理(类似于G1的分区Region);
  • ZGC为了能够快速对对象进行并发标记和并发移动,对内存空间重新进行了划分,这就是ZGC中新引入的指针染色;
  • 仅支持 Linux 64 位系统,不支持 32 位平台。因此也不支持压缩指针。
  • 同时ZGC为了能更加高效地管理内存,设计了物理内存和虚拟内存两级内存管理。
  • 支持NUMA-Aware内存分配:在NUMA(非统一内存访问架构)架构下,每个处理器核心有独立管理的本地内存,访问其他核心的内存较慢。ZGC通过优先在请求线程所在处理器的本地内存上分配对象,优化了内存访问效率。

1.1,传统对象地址

【传统GC指令地址设计】在ZGC出现之前,GC信息被保存在对象头的Mark Word当中。64位JVM主要指的是JVM可以使用64位的地址空间(即64位指针),而不是每个对象的大小必须是64字节。

  • 对象头:
    • Mark Word
      • 存储对象的哈希码(如果是第一次计算哈希时会计算并缓存)。
      • GC状态:用于GC时标记对象的状态(如是否可达)。
      • 锁信息:在对象被锁定时存储锁的状态,如轻量级锁、重量级锁、偏向锁等。
    • Klass Pointer(类指针):指向该对象的类元数据,实际上是指向对象的类信息(Class对象)。该指针指向Class对象的内存地址,通过这个指针,JVM能够查找到该对象的类型信息,Class对象包含了该类的结构信息,比如类的字段、方法以及接口等。
  • 实例数据:实例数据部分存储对象的实际数据。即类中定义的实例变量(属性)。这些数据按照类的字段顺序在内存中排列。例如,如果一个类中有一个int类型和一个String类型的字段,实例数据部分就依次存储这两个字段的值。
  • 对齐填充:为了确保对象在内存中的对齐,JVM通常会对对象的内存布局进行填充。例如,32位机器上,通常要求对象的大小是8的倍数。如果对象的实际数据占用内存不满足对齐要求,JVM会插入额外的填充字节。
public class Person {int age;String name;
}

内存布局示意图(假设64位系统,按默认的内存布局):总共占用的内存可能是 8 + 8 + 4 + 8 = 28字节,但为了满足对齐要求,可能会有额外的填充字节,最终对象的大小通常会是32字节(假设JVM默认按照8字节对齐)。

1.2,ZGC内存管理 

【ZGC虚拟地址空间】HotSpot虚拟机的几种收集器有不同的标记实现方案,有的把标记直接记录在对象头上(如Serial收集器),有的把标记记录在与对象相互独立的数据结构上(如G1、Shenandoah使用了一种相当于堆内存的1/64大小的,称为BitMap的结构来记录标记信息),ZGC的染色指针直接把标记信息记在引用对象的指针上(这个时候,与其说可达性分析是遍历对象图来标记对象,还不如说是遍历“引用图”来标记“引用”了),通过这四个标志位,JVM 可以从指针上直接看到对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集(Remapped)、是否需要通过 finalize 方法来访问到(Finalizable)等信息。无需进行对象访问就可以获得 GC 信息,这大大提高了 GC 效率。 🚀🚀🚀

  • Remappd:对象被重新映射到新内存位置(移动过)。
  • M1:上次GC标识过。
  • M0:本次GC标识过。

注意:X86_64 处理器硬件的限制,目前 X86_64 处理器地址线只有 48 条(CPU设计时位为了降低成本,仅支持48位地址),除去 4 位染色指针,剩余可用对象地址 44 位,理论上支持 16TB 的内存。

【问题】那为啥总说ZGC最大支持内存是4TB?

【答案】目前支持的 4TB 只是人为的限制,主要是为了平衡性能、稳定性和实际需求(压根没有4TB的机器)。

 6                 4 4 4  4 4                                             03                 7 6 5  2 1                                             0
+-------------------+-+----+-----------------------------------------------+
|00000000 00000000 0|0|1111|11 11111111 11111111 11111111 11111111 11111111|
+-------------------+-+----+-----------------------------------------------+
|                   | |    |
|                   | |    * 41-0 Object Offset (42-bits, 4TB address space)
|                   | |
|                   | * 45-42 Metadata Bits (4-bits)  0001 = Marked0
|                   |                                 0010 = Marked1
|                   |                                 0100 = Remapped
|                   |                                 1000 = Finalizable
|                   |
|                   * 46-46 Unused (1-bit, always zero)
|
* 63-47 Fixed (17-bits, always zero)

【问题】Java虚拟机作为一个普普通通的进程,这样随意重新定义内存中某些指针的其中几位,操作系统是否支持?处理器是否支持?

【答案】程序代码最终都要转换为机器指令流交付给处理器去执行,处理器可不会管指令流中的指针哪部分存的是标志位,哪部分才是真正的寻址地址,只会把整个指针都视作一个内存地址来对待。Solaris/SPARC平台上,硬件层面直接支持虚拟地址掩码,能够轻松忽略染色指针中的标志位,从而简化了ZGC的设计。而在x86-64平台上,没有类似的硬件支持,ZGC设计者必须依赖其他的技术手段,主要是虚拟内存映射技术,以弥补这一缺陷。

【答案】ZGC仅支持64位系统,它把64位虚拟地址空间划分为多个子空间。当创建对象时,首先在堆空间申请一个虚拟地址,该虚拟地址并不会映射到真正的物理地址。同时,ZGC 会在 M0、M1、Remapped 空间中为该对象分别申请一个虚拟地址,且三个虚拟地址都映射到同一个物理地址。ZGC 就是通过这三个视图空间的切换,来完成并发的垃圾回收。

​假如你要去 “中山一路3号” 这个地址拜访一位朋友,根据你所处城市的不同,譬如在广州或者在上海,是能够通过这个“相同的地址”定位到两个完全独立的物理位置的,这时地址与物理位置是一对多关系映射。

1.3,读屏障

当程序尝试读取一个对象时,读屏障会触发以下操作:

  • 检查指针染色:读屏障首先检查指向对象的指针的颜色信息。
  • 处理移动的对象:如果指针表示对象已经被移动(例如,在垃圾回收过程中),读屏障将确保返回对象的新位置。
  • 确保一致性:通过这种方式,ZGC 能够在并发移动对象时保持内存访问的一致性,从而减少对应用程序停顿的需要。
// 伪代码示例,展示读屏障的概念性实现
Object* read_barrier(Object* ref) {//如果对象已经被移动,返回新地址if (is_forwarded(ref)) {return get_forwarded_address(ref); // 获取对象的新地址}return ref; // 对象未移动,返回原始引用
}

读屏障可能被GC线程和业务线程触发,并且只会在访问堆内对象时触发,访问的对象位于GC Roots时不会触发,这也是扫描GC Roots时需要STW的原因。

1.4,ZGC工作流程

【初始态】在 ZGC 中,内存被划分为固定大小的页面(通常是 2MB),这些页面用于存储对象和管理内存。

【初始标记】ZGC 标记所有从 GC Root 直接可达的对象。

【并发标记&重新映射】

  • 【初次GC】GC Root开始对堆中对象进行可达性分析。
  • 【二次GC】把上次GC "并发迁移" 阶段迁移的对象指针修正指向到新分区。

【再标记】标记上一次标记过程新产生的对象。

【并发转移准备】为对象转移做一些前置准备,比如引用处理、弱引用清理和重定位集选择等。

【初始转移】迁移根节点直接引用的对象到新分区,这个阶段需要停顿所有的应用线程(STW),但由于只迁移根节点直接引用的对象,所以停顿时间很短。

【并发转移】并发迁移“并发标记”阶段标记的对象到新分区(对象引用指针未修改,仍指向旧分区)。

其实,在标记阶段存在两个地址视图M0和M1,上面的过程显示只用了一个地址视图。之所以设计成两个,是为了区别前一次标记和当前标记第二次进入并发标记阶段后,地址视图调整为M1,而非M0。

【问题】为何并发转移阶段,对象已转移至新分区后,却没有修改线程栈上实际的引用,依然指向旧分区?

【答案】因为如果此时再扫描线程栈,修改引用地址,要扫描的量太大,效率太低。刚好下一个GC周期也要进行扫描标记,可以利用扫描标记的时间,同时把对象引用修正指向到新分区,以此提升效率,减少停顿时间。

【问题】并发转移阶段对象已迁移,但引用指针仍指向旧分区,如何保证旧分区被清理后对象仍然可以访问?

【答案】由于未修改对象引用指针,为防止旧分区被清理,导致对象找不到的问题,此处引入了读屏障和转发表。

  • 转发表记录了对象从旧位置到新位置的映射关系,实现类似一个hash表,key是旧分区的位置,value是新分区的位置,此时当访问旧位置的对象时,通过转发表可以获取新位置。这样可以避免在整个堆空间中更新对象引用的开销,因为只需要更新转发表中的条目即可。
  • 读屏障的作用是在读取对象引用时,检查对象的标记状态并获取转发表中的映射关系。通过读屏障,ZGC能够在读取对象引用时,将访问重定向到新位置,以确保对象的访问仍然有效。如下图:每次读取引用时会触发一次读屏障。

1.5,ZGC性能 

不仅应用在并发转移阶段,还应用在并发标记阶段:将对象设置为已标记,传统的垃圾回收器需要进行一次内存访问,并将对象存活信息放在对象头中;而在ZGC中,只需要设置指针地址的第42-45位即可,并且因为是寄存器访问,所以速度比访问内存更快。

【ZGC触发时机】

  • 阻塞内存分配请求触发:当垃圾来不及回收,垃圾将堆占满时,会导致部分线程阻塞。应当避免出现这种触发方式。日志中关键字是“Allocation Stall”。
  • 基于分配速率的自适应算法:最主要的GC触发方式,其算法原理可简单描述为”ZGC根据近期的对象分配速率以及GC时间,计算出当内存占用达到什么阈值时触发下一次GC”。通过ZAllocationSpikeTolerance参数控制阈值大小,该参数默认2,数值越大,越早的触发GC。通过调整此参数解决了一些问题。日志中关键字是“Allocation Rate”。
  • 基于固定时间间隔:通过ZCollectionInterval控制,适合应对突增流量场景。流量平稳变化时,自适应算法可能在堆使用率达到95%以上才触发GC。流量突增时,自适应算法触发的时机可能会过晚,导致部分线程阻塞。通过调整此参数解决流量突增场景的问题,比如定时活动、秒杀等场景。日志中关键字是“Timer”。
  • 主动触发规则:类似于固定间隔规则,但时间间隔不固定,是ZGC自行算出来的时机,服务因为已经加了基于固定时间间隔的触发机制,所以通过-ZProactive参数将该功能关闭,以免GC频繁,影响服务可用性。 日志中关键字是“Proactive”。
  • 预热规则:服务刚启动时出现,一般不需要关注。日志中关键字是“Warmup”。
  • 外部触发:代码中显式调用System.gc()触发。 日志中关键字是“System.gc()”。
  • 元数据分配触发:元数据区不足时导致,一般不需要关注。 日志中关键字是“Metadata GC Threshold”。

2,分代ZGC

2.1,JDK21在ZGC上的升级

【支持分代】增加了对分代的支持,提高垃圾回收的性能。

  • JDK21之前:ZGC 的堆内存也是基于 Region 来分布,不过 ZGC 是不区分新生代老年代的。
  • JDK21之后:分代ZGC为年轻和年老的对象保留不同的世代,这将使 ZGC 能够更频繁地收集年轻对象,因为年轻对象往往在很年轻时就会死亡。

【分代的必要性】在程序运行过程中很多对象生命期较短,对这些短生命期对象进行回收,可以回收很多内存空间;剩余那部分生命期较长的对象,一般也不会被回收掉,所以对这些长生命期对象进行回收,可以回收的内存就比较有限了。不应该对所有对象都一视同仁,对于那些生命期短的对象要经常回收,获取高收益,对于那些生命期长的对象尽量不要浪费时间去回收。

  • 配置参数简单:-XX:+UseZGC -XX:+ZGenerational +Xmx 64g
  • 自动调节:
    • 不需要配置 -Xmn (年轻代、老年代动态变化)

    • 不需要配置 -XTenuringThreshold (什么时候晋升老年代动态变化)

    • 不需要配置 -XX:InitiatingHeapOccupancyPercent (G1 混合回收)

    • 不需要配置  -XX:ConcGCThreads (GC线程数动态变化)

ZGC 采用一种称为彩色指针的技术。为了避免掩码指针的开销,ZGC 采用了多重映射技术。多重映射是指将多段虚拟内存映射到同一段物理内存。 ZGC使用Java堆的3个视图(“marked0”,“marked1”,“remapped”),即3种不同“颜色”的堆指针和同一个堆的3个虚拟内存映射。因此,操作系统可能会报告 3 倍大的内存使用量。例如,对于 512 MB 的堆,报告的已提交内存可能高达 1.5 GB,不包括堆以外的内存。注意:多重映射会影响报告的使用内存,但物理上堆仍将使用 512 MB 的 RAM。这有时会导致一个有趣的效果,即进程的 RSS 看起来大于物理 RAM 的数量。

2.2,如何解决ZGC的RSS指标翻3倍的问题?

【ZGC染色指针】如果读者了解过普通ZGC,就一定了解它的颜色指针,通过虚拟内存高位的4个bit位标志当前的引用垃圾回收状态。这4个bit位中,有一位没有使用,而其它3位同一时间内只有1位为1,但是不管哪位为1,都要指向相同的物理地址,也就映射3次,最终造成普通ZGC的RSS指标(RSS统计的虚拟内存地址)翻了3倍。

【分代ZGC染色指针】分代ZGC需要更多的标记位,如果还使用muli-map的方式,第一可用内存会因为多加标记位减少;第二RSS指标可能是实际使用内存的4*4*4(每代4个指针)倍?所以分代ZGC在把虚拟内存交给操作系统的时候,需要清除标记位。这也是为啥ZGC一开始不支持分代的原因。

  • 保存在内存中的Java对象引用地址是有颜色的。
  • 读取出来处理的时候,通过 Load Barrier 将颜色去掉,之后再去寻址。
  • 存储的时候,通过 Store Barrier 将颜色恢复。

PS:Load Barrier 和 Store Barrier 是 ZGC 消耗 CPU 大的一个重要原因。

​对象的有效地址为46位,相对于64位操作系统对用户空间47位的限制,只少了1位,比起普通ZGC多了2位(64TB)。同时颜色指针放在了低位,有12位之多。load color(R):染色指针,跟读屏障有关。

【操作系统地址-无色指针地址】当JVM把分代ZGC中的虚拟地址交给操作系统使用时,会去掉12位染色指针,转换为如下的形式。所以操作系统看到的就是标准的虚拟地址(RSS不会翻倍),这个过程通过读屏障来实现。

2.3,如何在不产生额外成本的情况下去除和恢复颜色?

【ZGC读屏障】检查指针颜色是否是好的;普通ZGC在度屏障中先加载地址(rbx寄存器中的地址转换为虚拟地址)到rax寄存器,然后通过颜色指针验证地址是否有效(testq),如果不是有效地址则进入slow_path中(remap操作完成对象指针修复,转变为有效地址,转发表)。由于指针信息直接给到了操作系统,所以普通ZGC需要将三个虚拟地址映射到同一个物理地址上。

【分代ZGC读屏障】分代ZGC先加载地址到rax寄存器中,然后右移address_shift位(右移位数于GC阶段有关),然后判断CF和ZF是否都为0(ja指令的作用),如果该条件成立,则进入slow_path完成对象指针修复(并发标记阶段的指针修复)。

【address_shift操作】右移最右移除的低位为1时CF为1,否则CF为0。右移操作得到的结果为全0,那么ZF为1,否则ZF为0。由于地址右移时不会得到全0结果,所以这里ZF可以认为是一个0常量。关键要看CF,而CF的结果由address_shift所决定。

​一共4中情况,分别对应于不同的GC阶段的有效地址,有效地址的4个R位中根据当前所处阶段,只有1位为1。在每种情况中address_shift的值恰好可以把墨绿色的唯一的1移除掉(绿色右侧的移除)。由于JVM中地址是按8对齐的,对于一个有效的地址来说最小为8,所以低3位一定为0(00001000=8),本着能省就省的宗旨,低3位的0和读标记区进行了重叠。

由于在读地址的时候把指针信息删除了,所以在写的时候,就要把信息恢复,分代ZGC不得不在写屏障完成这个操作。在写入的时候,12个染色指针都需要参与。

【ZGC写操作】普通ZGC写入的时候只是保存了地址信息。

【分代ZGC写操作】分代ZGC在写入时则多做了4个操作。前两个操作合起来就是检测地址是否需要处理,如果需要处理进入slow_path中处理,这里slow_path主要做了如下操作:

  • 并行年轻代SATB 染色;

  • 并行老年代SATB 染色;

  • 并行Remember Set 染色。

后两条指令这是把地址左移,然后把颜色指针还原。由此可见,在写入上必然会有性能损耗。

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

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

相关文章

OpenCV基于均值漂移算法(pyrMeanShiftFiltering)的水彩画特效

1、均值漂移算法原理 pyrMeanShiftFiltering算法结合了均值迁移(Mean Shift)算法和图像金字塔(Image Pyramid)的概念,用于图像分割和平滑处理。以下是该算法的详细原理: 1.1 、均值迁移(Mean …

【数学】概率论与数理统计(五)

文章目录 [toc] 二维随机向量及其分布随机向量离散型随机向量的概率分布律性质示例问题解答 连续型随机向量的概率密度函数随机向量的分布函数性质连续型随机向量均匀分布 边缘分布边缘概率分布律边缘概率密度函数二维正态分布示例问题解答 边缘分布函数 二维随机向量及其分布 …

四 BH1750 光感驱动调试2

之前调通了用户态接口,android 使用还是不方便,要包装jni使用。 这里集成了内核 iio 驱动 ,提供 sys-fs 文件接口 可供固件以及 ANDROID 应用层使用 一 驱动集成 : 1.1 dts 修改 修改文件 : kernel/arch/arm64/boot/dts/rockchip/rp-rk3568.dts 在 i2c5 增加设备,如…

Redis持久化双雄

Redis持久化 Redis 的持久化是指将内存中的数据保存到硬盘,以防止服务器宕机导致数据丢失的机制。 redis 提供了两种持久化的方式,分别是RDB(Redis DataBase)和AOF(Append Only File)。 RDB,简…

工业视觉2-相机选型

工业视觉2-相机选型 一、按芯片类型二、按传感器结构特征三、按扫描方式四、按分辨率大小五、按输出信号六、按输出色彩接口类型 这张图片对工业相机的分类方式进行了总结,具体如下: 一、按芯片类型 CCD相机:采用电荷耦合器件(CC…

数字证书管理服务

阿里云数字证书管理服务(Aliyun Certificate Management Service, ACM)是一种云端服务,专门用于帮助企业管理和颁发数字证书。数字证书是网络安全中的重要组成部分,它可以确保通信的安全性、身份认证以及数据的完整性。通过阿里云…

《跟我学Spring Boot开发》系列文章索引❤(2025.01.09更新)

章节文章名备注第1节Spring Boot(1)基于Eclipse搭建Spring Boot开发环境环境搭建第2节Spring Boot(2)解决Maven下载依赖缓慢的问题给火车头提提速第3节Spring Boot(3)教你手工搭建Spring Boot项目纯手工玩法…

Zookeeper概览

个人博客地址:Zookeeper概览 | 一张假钞的真实世界 设计目标 简单的:方便使用以实现复杂的业务应用。复制式的:跟Zookeeper协调的分布式进程一样,它也是在一组服务器上复制的。集群的每个节点间互相知道。它们维护一个状态数据在…

播放音频文件同步音频文本

播放音频同步音频文本 对应单个文本高亮显示 使用audio音频文件对应音频文本资源 音频文本内容(Json) [{"end": 4875,"index": 0,"speaker": 0,"start": 30,"text": "70号二啊,","tex…

React中ElementFiber对象、WorkInProgress双缓存、ReconcileRenderCommit、第一次挂载过程详解

基础概念 Element对象与Fiber对象 Element对象与Fiber对象 Element 对象 定义 React 的 Element 对象是一个描述用户界面(UI)的普通 JavaScript 对象,通常由 React.createElement 或 JSX 语法生成。 作用 它是 React 应用中的一种描述 …

【airtest】自动化入门教程Poco元素定位

1. 前言 本文将详细讲解Poco控件定位的各种方式,利用这些方法可以帮助我们编写出目标控件的定位脚本。我们在IDE录制的poco脚本,常见的都是类似 poco(“star_single”).click()这样的脚本,其中 poco(“star_single”) 这块就属于Poco控件定位…

2025年01月13日Github流行趋势

1. 项目名称:Jobs_Applier_AI_Agent 项目地址url:https://github.com/feder-cr/Jobs_Applier_AI_Agent项目语言:Python历史star数:25929今日star数:401项目维护者:surapuramakhil, feder-cr, cjbbb, sarob…

[免费]SpringBoot+Vue新能源汽车充电桩管理系统【论文+源码+SQL脚本】

大家好,我是java1234_小锋老师,看到一个不错的SpringBootVue新能源汽车充电桩管理系统,分享下哈。 项目视频演示 【免费】SpringBootVue新能源汽车充电桩管理系统 Java毕业设计_哔哩哔哩_bilibili 项目介绍 随着信息化时代的到来&#xff0…

【C++】字符串中的 insert 方法深层分析

博客主页: [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 💯前言💯一、基础知识:insert 方法概述功能描述函数原原型基本规则 💯二、例子解析例子 1:插入一个 std::string分析 例子 2:插入一个…

G-Star Landscape 2.0 重磅发布,助力开源生态再升级

近日,备受行业瞩目的 G-Star Landscape 迎来了其 2.0 版本的发布,这一成果标志着 GitCode 在开源生态建设方面又取得了重要进展。 G-Star Landscape仓库链接: https://gitcode.com/GitCode-official-team/G-Star-landscape 2024 GitCode 开…

Sui Move:基本概览一

Module (模块) Move 代码被组织成模块, 可以把一个模块看成是区块链上的一个智能合约 可以通过调用这些模块中的函数来与模块进行交互,可以通过事务或其他 Move 代码来实现, 事务将被发送到并由Sui区块链进行处理,一旦执行完成,结果的更改将…

不同方式获取音频时长 - python 实现

DataBall 助力快速掌握数据集的信息和使用方式,会员享有 百种数据集,持续增加中。 需要更多数据资源和技术解决方案,知识星球: “DataBall - X 数据球(free)” -------------------------------------------------------------…

25年无人机行业资讯 | 1.1 - 1.5

25年无人机行业资讯 | 1.1 - 1.5 中央党报《经济日报》刊文:低空经济蓄势待发,高质量发展需的平衡三大关系 据新华网消息,2025年1月3日,中央党报《经济日报》发表文章指出,随着国家发展改革委低空经济发展司的成立&a…

frp内网穿透

frp CS搭建socks隧道 kali当作客户端,vps当作服务端,webshell机器上传后门,CS上线,使用CS自带socks搭建隧道 选择socks代理 服务端启动:frps -c frpssocks.toml serverAddr "47.104.216.23" serverPort…

【JVM-2.2】使用JConsole监控和管理Java应用程序:从入门到精通

在Java应用程序的开发和运维过程中,监控和管理应用程序的性能和资源使用情况是非常重要的。JConsole是Java Development Kit(JDK)自带的一款图形化监控工具,它可以帮助开发者实时监控Java应用程序的内存、线程、类加载以及垃圾回收…