Shenandoah
JDK 支持
JDK8, JDK11, JDK13
与 G1
相似
- 基于 Region 的堆内存布局。
- 存放大对象的 Humongous Region。
- 默认优先回收价值最大的 Region。
改进
- Shenandoah 支持并发的整理算法,G1 的整理阶段虽是多线程并行,但无法与用户程序并发执行;
- 默认不使用分代收集理论;
- 使用连接矩阵 (Connection Matrix)记录跨 Region 的引用关系,替换掉了 G1 中的记忆级 (Remembered Set),内存和计算成本更低。
第一行的X应该在[3, 1]的位置
工作过程
- Initial Marking(初始标记)
- 与G1一致, 首先标记与GC Roots直接关联的对象, 此阶段仍会"Stop The World", 但停顿时间与堆大小无关, 只与GC Roots的数量相关。
-
Concurrent Marking(并发标记)
- 与G1一致, 遍历对象图, 标记出全部可达的对象, 此阶段是与用户线程一起并发的, 时间长短取决于堆中存活对象的数量以及图的结构复杂程度。
- Final Marking(最终标记)
- 与G1一致, 处理剩余的原始快照(SATB)扫描, 并在此阶段统计出回收价值最高的Region, 将这些Region构成一组回收集(Collection Set)。
- 最终标记阶段也会有一小段短暂的停顿。
- Concurrent Cleanup(并发清理)
- 此阶段用于清理那些整个区域内连一个存活对象都没有找到的Region(Immediate Garbage Region)
-
Concurrent Evacuation(并发回收)
- 在此阶段, Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region中。
- 重点是复制对象过程不冻结用户线程而是与用户线程并行, 这一实现有很大的技术屏障, Shenandoah 通过读屏障和 Brooks Pointers(转发指针) 解决了此困难。
- 并发回收阶段运行时间长短取决于回收集的大小。
- Initial Update Reference(初始引用更新)
- 并发回收阶段复制对象结束后, 还需要把堆中所有指向旧对象的引用修正到复制后的新地址, 此操作称为引用更新。
- 实际上此阶段并没有做什么实际的处理, 只是为了建立一个线程集合点, 确保所有并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务而已。
- 初始引用更新时间很短, 会产生一个非常短暂的停顿。
-
Concurrent Update Reference(并发引用更新)
- 真正开始引用更新操作, 此阶段是与用户线程并发的, 时间长短取决于内存中涉及的引用数量的多少。
- 与并发标记不同, 不需要再沿着对象图来搜索, 只需要按照内存物理地址的顺序, 线性地搜索出引用类型, 把旧值改为新值即可。
- Final Update Reference(最终引用更新)
- 处理了堆中的引用更新后, 还需要修正存于GC Roots中的引用。
- 此阶段是Shenandoah的最后一次停顿, 停顿时间只与GC Roots数量相关。
- Concurrent Cleanup(并发清理)
- 经过并发回收和引用更新之后, 整个回收集中所有的Region已再无存活对象, 这些Region都变成了Immediate Garbage Regions了。
- 最后再调用一次并发清理过程来回收这些Region的内存空间, 供以后新对象分配使用。
性能
shenandoah 的高并发度让它实现了超低的停顿时间,但是更高的复杂度也伴随着更高的系统开销,这在一定程度上会影响吞吐量,下图是 Shenandoah 与之前各种收集器在停顿时间维度和系统开销维度上的对比:
转发指针 Brooks Pointer
对象移动方案
实现对象移动与用户程序并发的一种解决方案。
-
传统方案:在被移动对象原有的内存上设置保护陷阱(Memory Protection Trap),当用户程序访问到归属于旧对象的内存空间就会产生自陷中断,进入预设好的异常处理器中,再由其中的代码逻辑把访问转发到复制后的新对象上。
-
属于非操作系统层面的操作,会导致用户态和内核态的频繁切换。
-
转发指针:在原有对象布局结构的最前面统一增加一个新的引用字段,在正常不处于并发移动的情况下,该引用指向对象自己。
- 当对象拥有了一份新的副本时,只需要将旧对象转发指针指向新对象,就可以将所有对旧对象的访问转发到新的副本上。
- 由于需要旧对象作为桥梁,所以此阶段要在旧对象被回收前完成。
转发指针的并发问题
- 在并发读操作中,读取旧对象与读取新对象得到的信息应该都是一致的。
- 在并发写操作中,要保证写操作只能发生在新复制的对象上,而不是写入旧对象的内存中。
- 让收集器或者用户线程对转发指针的访问只有其中之一能够成功,另外一个必须等待,避免两者交替进行。该过程通过 CAS 来完成。
执行频率问题
访问对象会产生很大的消耗,Shenandoah 同时设置了读、写屏障去覆盖全部对象的访问操作。
读屏障的代价比写屏障高,而对象读取的频率要比对象写入的频率高,所以读屏障就多,那么代价就高,在 JDK 13 中将内存屏障改为引用访问屏障。
- 引用屏障:内存屏障只拦截对象中数据类型为引用类型的读写操作,不需要处理原生数据类型等其他非引用字段的读写。
ZGC
基于 Region 内存布局,暂时不设分代,使用读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法,以低延迟为首要目标。
主要特征
- 基于Region内存布局
- 不设分代
- 使用了读屏障, 染色指针和内存多重映射等技术来实现可并发的标记-整理算法
- 以低延迟为首要目标
内存布局
ZGC也采用基于Region的堆内存布局, 但与它们不同的是, ZGC的Region具有动态性: 动态创建和销毁, 以及动态的区域容量大小。在x64硬件平台下, ZGC的Region可以有以下三类容量:
- Small Region
- 容量固定为2MB, 用于放置小于256KB的小对象。
- Medium Region
- 容量固定为32MB, 用于放置大于等于256KB但小于4MB的对象。
- Large Region
- 容量不固定, 可以动态变化, 但必须为2MB的整数倍, 用于放置4MB或以上的大对象。
- 每个大型Region中只会存放一个大对象, 它虽名为Large Region, 但它的实际容量完全有可能小于中型Region。
- 大型Region在ZGC的实现中是不会被重分配的, 因为复制一个大对象的代价非常高昂。
染色指针
- 在64位系统中, 理论可以访问的内存高达16EB。实际上基于需求, 性能, 和成本考虑, 在AMD64架构中只支持到52位(4PB)的地址总线和48位(256TB)的虚拟地址空间, 目前64位的硬件实际能够支持的最大内存只有256TB。此外操作系统还有自己的约束, 64Linux系统分别支持47位(128TB)的进程虚拟地址和46位(64TB)的物理地址空间, 64位的Windows系统只支持44位(16TB)的物理地址空间。
- 虽然Linux下64位指针的高18位不能用来寻址, 剩余的46位指针所能支持的64TB内存在今天仍能够充分满足大型服务器需要。而ZGC则利用了剩下的46位指针的高4位提取出来用于存储四个标志信息。
- 通过这些标志位, 虚拟机可以直接从指针中看到其引用对象的状态, 是否进入重分配集, 是否通过finalize()方法才能被访问到。
- 由于进一步压缩了原本只有46位的地址空间, ZGC能够管理的内存不可以超过4TB。
优势
- 染色指针可以使得一旦某个Region的存活对象被移走之后, 此Region立即就能够被释放和重用掉, 而不必等待整个堆中所有指向该Region的引用都被修正后才能清理。
- 染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量。
- 染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据, 以便日后进一步提高性能。
工作过程
-
Concurrent Mark(并发标记)
- 遍历对象图做可达性分析的阶段, 前后也要经过类似于G1, Shenandoah 的初始标记, 最终标记的短暂停顿。
- 与G1, Shenandoah不同的是, ZGC的标记是在指针上而不是在对象上进行的, 标记阶段会更新染色指针中的Marked0、Marked1标志位。
-
Concurrent Prepare for Relocate(并发预备重分配)
- 此阶段需要根据特定的查询条件统计出本次收集过程要清理哪些Region, 将这些Region组成重分配集(Relocation Set)。
- Concurrent Relocate(并发重分配)
- 是ZGC执行过程中的核心阶段, 此过程要把重分配集中的存活对象复制到新的Region上, 并为重分配集中的每个Region维护一个转发表(Forward Table), 记录从旧对象到新对象的转向关系。
- 由于染色指针的存在, ZGC能仅从引用上就明确得知一个对象是否处于重分配集之中。如果用户线程此时并发访问了位于重分配集中的对象, 这次访问将会被预置的内存屏障截获, 然后立即根据Region上的转发表记录将访问转发到新复制的对象上, 并同时修正该引用的值, 使其直接指向新对象, 此即为Self-Healing(自愈)[只有第一次访问旧对象会陷入转发]。
- Concurrent Remap(并发重映射)
- 修正整个堆中指向重分配集中旧对象的所有引用。
- 重映射清理这些旧引用的主要目的是为了不变慢, 并不是很迫切。
- ZGC将并发重映射阶段要做的工作, 合并到了下一次垃圾收集循环中的并发标记阶段里去完成, 从而节省了一次遍历对象图的开销。
拓展阅读
⭐️ 如果对你有帮助,请点个赞????
参考资料
《深入理解Java虚拟机-第三版》
cyc 2018
JavaGuide
《Java虚拟机原理图解》4.JVM机器指令集