前言:
🥂介绍染色指针之前,大家先回顾以下上一篇文章 《JVM垃圾收集-三色标记》,大部分追踪式垃圾收集器在并发标记阶段都采用了三色标记技术。但也有例外,比如:ZGC收集器有一个标志性的设计,就是采用了染色指针技术(Colored Pointer,其他类似的技术中可能将它称为Tag Pointer或者Version Pointer)。下面就来介绍一下染色指针。
应用场景
🌴🌴之前,如果我们要在对象上存储一些额外的、只供收集器或者虚拟机本身使用的数据,通常会在对象头中增加额外的存储字段,如对象的哈希码、分代年龄、锁记录等就是这样存储的。比如 64 位的 JVM,对象头的 Mark Word 中保存的信息如下图:
🎈这种记录方式在有对象访问的场景下是很自然流畅的,不会有什么额外负担。但如果有一些对象根本就不会去访问它,但又希望得知该对象的某些信息的场景呢?
我们就有这样的场景——追踪式收集算法的标记阶段就可能存在只跟指针打交道而不必涉及指针所引用的对象本身的场景。
🌰例如 对象标记的过程中需要给对象打上三色标记,这些标记本质上就只和对象的引用有关,而与对象本身无关——某个对象只有它的引用关系能决定它存活与否,对象上其他所有的属性都不能够影响它的存活判定结果。
👩HotSpot虚拟机的几种收集器有不同的标记实现方案:
- 📍 把标记直接记录在对象头上(Serial收集器)
- 📍 把标记记录在与对象相互独立的数据结构上(G1、Shenandoah使用了一种相当于堆内存的1/64大小的,称为BitMap的结构来记录标记信息)
- 📍 ZGC的染色指针直接把标记信息记在引用对象的指针上(这个时候,与其说可达性分析是遍历对象图来标记对象,还不如说是遍历“引用图”来标记“引用”了。)
染色指针
✨染色指针是一种直接将少量额外的信息存储在指针上的技术。在 64 位 Linux 中,对象指针是 64 位,如下图:
👉🏻在这个 64 位的指针上,高 18 位都是 0,暂时不用来寻址。剩余的 46 位指针所能支持内存可以达到 64TB ,这可以满足多数大型服务器的需要了。不过 ZGC 并没有把 46 位都用来保存对象信息,而是用高 4 位保存了四个标志位,导致 ZGC 可以管理的最大内存不超过 4 TB 。
⭐⭐通过这四个标志位,JVM 可以从指针上直接看到对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集(Remapped)、是否需要通过 finalize 方法来访问到(Finalizable)等信息。
无需进行对象访问就可以获得 GC 信息,这大大提高了 GC 效率。 🚀🚀🚀
🍺可以看到染色指针的优点已经很明显了。不过,要使用染色指针有一个必须解决的前置问题:Java虚拟机作为一个普普通通的进程,这样随意重新定义内存中某些指针的其中几位,操作系统是否支持?处理器是否支持?🤷
👩🏻程序代码最终都要转换为机器指令流交付给处理器去执行,处理器可不会管指令流中的指针哪部分存的是标志位,哪部分才是真正的寻址地址,只会把整个指针都视作一个内存地址来对待。
🎈在我们常用的x86-64平台上是不支持重新定义机器指令的,ZGC设计者们就采用了虚拟内存映射技术来解决这个问题。
虚拟内存映射
🍬在x86平台上,处理器会使用分页管理机制把线性地址空间和物理地址空间分别划分为大小相同的块,这样的内存块被称为“页”(Page)。通过在线性虚拟空间的页与物理地址空间的页之间建立的映射表,分页管理机制会进行线性地址到物理地址空间的映射,完成线性地址到物理地址的转换。
✨简单的理解为:用 mmap 把不同的虚拟内存地址映射到同一个物理内存地址上。 如下图所示:
🍋ZGC 为了解决上面的寻址地址问题,使用了虚拟内存映射技术,把同一块儿物理内存映射为 Marked0、Marked1 和 Remapped 三个虚拟内存。
🍹当应用程序创建对象时,会在堆上申请一个虚拟地址,这时 ZGC 会为这个对象在 Marked0、Marked1 和 Remapped 这三个视图空间分别申请一个虚拟地址,这三个虚拟地址映射到同一个物理地址。
🍦Marked0、Marked1 和 Remapped 这三个虚拟内存作为 ZGC 的三个视图空间,在同一个时间点内只能有一个有效。ZGC 就是通过这三个视图空间的切换,来完成并发的垃圾回收。
🙇好了关于染色指针的就介绍到这里,大家有什么疑问欢迎评论区讨论。
PS: 在 JDK 15 中 ,已经可以通过指令 –XX:+UseZGC 来启用采用染色指针技术的 ZGC收集器了。
🚀🚀🚀