「JVM 内存管理」低延迟的 Shenandoah GC 与 ZGC

同时在内存占用(Footprint),吞吐量(Throughput),延迟(Latency)三方面表现得最优,才能称得上完美的垃圾收集器,但这几乎是不可能的(不可能三角,三元悖论,通常最多可以兼顾两项);

随着硬件规格和性能的不断提升,前两者已经变得不那么重要,而 Latency 反倒可能因硬件规格提升而变得更差,因此 Latency 是最被重视的性能指标;

「JVM 内存管理」低延迟的 Shenandoah GC 与 ZGC_第1张图片

文章目录

      • 1. Shenandoah GC
      • 2. ZGC

1. Shenandoah GC

基于标记整理(从 Region 看是标记复制)算法的不分代 GC;志在能让任意大小的堆的 GC 时间控制在 10 ms 以内;

Shenandoah 是唯一非官方出品的 HotSpot GC,也是唯一出现在 OpenJDK 却不在 OracleJDK 上的 GC;

Shenandoah vs. G1

  1. Shenandoah 支持并发整理;
  2. Shenandoah 不分代,分区回收性价比更高;
  3. Shenandoah 将记忆集改为更低耗的连接矩阵(Connection Matrix),减低消耗,并减少了伪共享问题;

连接矩阵,一张记录 Region 间引用关系的二维表,如果 Region N 引用了 Region M,则在表格的 N 行 M 列打上标记;

Shenandoah GC 的 9 个阶段

  • 初始标记(Inital Marking),标记 GC Roots 直接引用的对象;STW
  • 并发标记(Concurrent Marking),遍历对象图,标记全部可达对象,与用户线程并发进行;时间长短取决于存活对象数量和引用关系复杂度;
  • 最终标记(Final Marking),处理 SATB(原始快照),并统计回收价值最高的 Region,将之构造成回收集(Collection Set);STW
  • 并发清理(Concurrent Cleanup),清理无存活对象的 Region(Immediate Garbage Region);
  • 并发回收(Concurrent Evacuation),把回收集中存活对象复制一份到空 Region 中,并通过读屏障转发指针(Brooks Pointers)解决复制时用户线程的并发问题;
  • 初始引用更新(Inital Update Reference),一个线程集合点,确保并发回收阶段中所有 GC 线程以完成被分配的复制任务,并无实质动作;STW
  • 并发引用更新(Concurrent Update Reference),把指向旧对象的引用修正到复制后的新对象的地址;不需要搜索整个引用链,只需要线性搜索内存物理地址;与用户线程并发进行,时间长短取决于涉及的引用数量;
  • 最终引用更新(Final Update Reference),修正存在于 GC Roots 中的引用;STW
  • 并发清理(Concurrent Cleanup),经过并发回收和引用更新,整个回收集所有 Region 都变成了 Immediate Garbage Region,此时再进行一次并发清理回收这些 Region 的所有空间;

三个最重要的并发阶段

「JVM 内存管理」低延迟的 Shenandoah GC 与 ZGC_第2张图片

  • 黄色,被选入回收集的 Region;
  • 绿色,还存活的对象;
  • 蓝色,可以用来分配对象的内存 Region;

并行整理的核心原理

  • 旧的解决方案,通过保护陷阱,让用户线程访问到归属于旧对象的空间时自陷中断,进入预设的异常处理器将访问转发到复制后的新对象上;需要操作系统层面的直接支持,频繁切换用户态到核心态,代价太大;

  • Brooks Pointer,转发指针,Forwarding Pointer,Indirection Pointer;在对象布局的最前面统一增加一个引用字段(Brooks Pointer),正常情况指向自己,当发生并发回收并完成复制后,指针指向自己的副本(类似对象引用中的句柄访问,不同的是句柄统一存储在句柄池,而 Brooks Pointer 存储在每个对象的头部);

通过转发指针会给对象访问带来一次额外的转向开销,对象定位会被频繁使用,这笔开销是不可忽视的,但比起内存保护陷阱好了很多;

转发指针的多线程竞争问题

  • 并发读,读到新旧对象的字段,结果都一样;
  • 并发写,必须保证写操作只能发生在新复制的对象上;这需要保证 GC 线程与用户线程对转发指针的访问同一时间只能有一个成功,另一个必须等待;这里通过 CAS(Compare And Swap)保证并发访问的正确性;

要覆盖全部对象访问操作(读取、写入、比较、哈希值计算、对象加锁等),Shenandoah 同时设置了读、写屏障拦截处理;

Shenandoah 的诟病之处

  • 由于对象读取频率远高于对象写入,庞大数量的读屏障带来了性能开销;所以 Shenandoah 计划在 JDK 13 引入引用访问屏障(Load Reference Barrier,内存屏障只拦截引用类型的对象,对原生数据类型对象的读取直接放行);

Shenandoah 的表现

通过 2016 年周志明老师使用 ES 对 200GB 维基百科数据的行索引测试来看,Shenandoah 的 GC Pause 时间确实有了质的飞跃(最大停顿 89.79ms,G1 最大停顿为 1.24s,CMS 最大停顿为 4.29s;平均停顿 53.01 ms,G1 平均停顿为 450.12ms,CMS 平均停顿为 852.25ms),但吞吐方面下降明显,总运行时间是所有测试 GC 中最长的;

2. ZGC

Z Garbage Collector,基于标记整理(从 Region 看是标记复制)算法的无分代的 GC;堆内存布局也是基于 Region(也称 Page 或 ZPage),使用了读屏障、染色指针、内存多重映射等技术实现;

ZGC 的 Z 无专业含义,其目标与 Shenandoah 高度相似,从实现思路上看,Shenandoah 是 G1 的继承者,而 ZGC 则是 Azul System 独步天下的 PGC(Pauseless GC,运行在 Azul VM)和 C4(Concurrent Continuously Compacting Collector,运行在 Zing VM)的同胞兄弟;

ZGC 的 Region

ZGC 的 Region 具有动态性(动态创建和销毁、动态的容量大小),容量可分为三类;

  • 小型 Region(Small Region),固定 2MB,放置小于 256KB 的对象;
  • 中型 Region(Medium Region),固定 32MB,放置大于等于 256KB,小于 4MB 的对象;
  • 大型 Region(Large Region),大于等与 4MB,大小不固定(2MB 的整数倍),放置大于等于 4MB 的对象;一个 Region 只放一个对象;(避免大对象的复制,这代价太高);

ZGC 的四大步骤

四个阶段间隙会出现短暂停顿的小阶段,这里忽略;

「JVM 内存管理」低延迟的 Shenandoah GC 与 ZGC_第3张图片

  • 并发标记(Concurrent Mark),遍历对象引用链做可达性分析;与 G1、Shenandoah 一样,前后也需要初始标记,最终标记的短暂停顿,与之不同的是 ZGC 的标记在指针上而不在对象上,标记阶段会更新指针的 Marked 0,Marked 1 标志位;
  • 并发预备重分配(Concurrent Prepare for Relocate),跟进特定查询条件获得本次 GC 需要清理的 Region,将之组成重分配集(Relocation Set),只是决定哪些对象(存活的)会被复制到其他 Region;此外 JDK 12 的 ZGC 在此阶段支持了类卸载和弱引用处理;与 G1 不同的是,ZGC 步骤收益优先的回收计划,而是扫描全部 Region,省去了 G1 中记忆集的维护成本;
  • 并发重分配(Concurrent Relocate),把重分配集中的存活对象复制到新的 Region,并为重分配集的每个 Region 维护一张转发标(Forward Table),记录从旧对象到新对象的转向关系(从染色指针得知);
    • 自愈(Self Healing),通过内存屏障截获用户线程的并发访问,让后通过 Region 的转发表将访问转发给新对象,同时修正引用指针;只有第一次访问会走到旧对象,优于 Shenandoah 的 Brooks Pointer;
  • 并发重映射(Concurrent Remap),修正整个 Heap 中指向重分配集中旧对象的所有引用,相比 Shenandoah 的并发引用更新,ZGC 的并发重映射并不那么迫切(因为旧指针的自愈性,执行并发重映射的目的是减少变慢的可能以及回收转发表的空间);ZGC 会将这一步合并到下一次 GC 循环中并发标记阶段去执行(可以在一次遍历对象应用链中完成这两件事);修正完指针后,记录新旧关系的转发表就可以被释放了;

ZGC 的 并发整理技术

  • 染色指针技术(Colored Pointer,Tag Pointer,Version Pointer),只有对象的引用关系能决定对象的存活与否,对象的其他属性都不影响其存活判定;因此 ZGC 的染色指针直接将标记信息记录在引用对象的指针上,也就是遍历引用链时标记引用;将 64 位操作系统中可用的 46 位的高 4 位用来存储 4 个标志信息(引用对象的三色标记状态、是否进入了重分配集、是否只能通过 finalize 方法才能被访问);这将限制 ZGC 只支持 4TB 内存,不支持 32 位平台,不支持压缩指针(-XX:+UseCompressedfOops)等;

染色指针的三大优势

  • 一旦 Region 的存活对象被移走,这个 Region 可以立即释放或重用,不必等所有执行该 Region 的引用都修正后再清理,但 Region 的转发表还不能释放(自愈性);
  • 大幅减少 GC 过程中内存屏障的使用,让 ZGC 只使用读屏障,未使用任何写屏障(通过染色指针替代写屏障实现对象引用变更);降低了 GC 对吞吐量的影响;
  • 可以作为一种可扩展的存储结构,记录更多涉及对象标记、重定位等的数据,进一步提升性能(目前 64 位的高 18 位还可以进一步开发);

x86-64 平台不提供类似虚拟地址掩码的黑科技,无法直接解析染色指针,这里就需要借助虚拟内存映射技术(ZGC 使用的是多重映射将多个不同的虚拟内存地址映射到同一个物理内存地址上,这甚至可能对大对象的复制有额外好处);

ZGC vs. G1、Shenandoah

  • G1 需要写屏障维护记忆集以处理跨代指针,实现 Region 的增量回收,ZGC 既无记忆集,也无分代,甚至无 CMS 的独立卡表;
  • ZGC 能承受的对象分配速度不会太高,高速分配对象会产生大量浮动垃圾,这些对象只能每次延迟到下一轮 GC,从而加大每一轮 GC 的耗时;解决该问题的根本是引入分代(分代的 C4 的分配速度比不分代的 PGC 提升了十倍之多);
  • ZGC 支持 NUM A-Aware(Non-Uniform Memory Access,非统一内存访问架构),优先尝试在请求线程当前所处的处理器的本地内存上分配对象,这比访问其他处理器核心管理的内存要快得多(Parallel Scavenge 也支持 NUMA);

ZGC 的表现(SPECjbb 2015 测试)

  • ZGC 的弱项吞吐量达到 Parallel Scavenge 的 99%,超越 G1(设定关注吞吐量时 ZGC 甚至反超 Parallel Scavenge);
  • ZGC 的 Average Pause、P95、P99、P999 皆在 10ms 以内,直接秒杀 Parallel Scavenge 和 G1 的秒级停顿;

ZGC 还未完成所有特性(如暂不提供全平台的支持,目前仅支持 Linux/x86-64,暂不支持与新的 Graal 编译器配合工作等),稳定性和性能调优方面也还在进行,待其成熟,相信会成为服务端、大内存、低延迟引用的首选 GC;


上一篇:「JVM 内存管理」7 款经典 GC
下一篇:「JVM 内存管理」GC 评估与选择

PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!


参考资料:

  • [1]《深入理解 Java 虚拟机》

你可能感兴趣的:(《JVM,体系梳理》,jvm,java,开发语言,性能优化)