「字节跳动」厉害!代表Java未来的ZGC深度剖析( 三 )


服务器的NUMA架构在中大型系统上一直非常盛行 , 也是高性能的解决方案 , 尤其在系统延迟方面表现都很优秀 。 ZGC是能自动感知NUMA架构并充分利用NUMA架构特性的 。
Colored PointersColored Pointers , 即颜色指针是什么呢?如下图所示 , ZGC的核心设计之一 。 以前的垃圾回收器的GC信息都保存在对象头中 , 而ZGC的GC信息保存在指针中 。 每个对象有一个64位指针 , 这64位被分为:

  • 18位:预留给以后使用;
  • 1位:Finalizable标识 , 次位与并发引用处理有关 , 它表示这个对象只能通过finalizer才能访问;
  • 1位:Remapped标识 , 设置此位的值后 , 对象未指向relocation set中(relocation set表示需要GC的Region集合);
  • 1位:Marked1标识;
  • 1位:Marked0标识 , 和上面的Marked1都是标记对象用于辅助GC;
  • 42位:对象的地址(所以它可以支持2^42=4T内存):
通过对配置ZGC后对象指针分析我们可知 , 对象指针必须是64位 , 那么ZGC就无法支持32位操作系统 , 同样的也就无法支持压缩指针了(CompressedOops , 压缩指针也是32位) 。
Load Barriers这个应该翻译成读屏障(与之对应的有写屏障即Write Barrier , 之前的GC都是采用Write Barrier , 这次ZGC采用了完全不同的方案) , 这个是ZGC一个非常重要的特性 。 在标记和移动对象的阶段 , 每次「从堆里对象的引用类型中读取一个指针」的时候 , 都需要加上一个Load Barriers 。 那么我们该如何理解它呢?看下面的代码 , 第一行代码我们尝试读取堆中的一个对象引用obj.fieldA并赋给引用o(fieldA也是一个对象时才会加上读屏障) 。 如果这时候对象在GC时被移动了 , 接下来JVM就会加上一个读屏障 , 这个屏障会把读出的指针更新到对象的新地址上 , 并且把堆里的这个指针“修正”到原本的字段里 。 这样就算GC把对象移动了 , 读屏障也会发现并修正指针 , 于是应用代码就永远都会持有更新后的有效指针 , 而且不需要STW 。 那么 , JVM是如何判断对象被移动过呢?就是利用上面提到的颜色指针 , 如果指针是Bad Color , 那么程序还不能往下执行 , 需要「slow path」 , 修正指针;如果指针是Good Color , 那么正常往下执行即可:
?
这个动作是不是非常像JDK并发中用到的CAS自旋?读取的值发现已经失效了 , 需要重新读取 。 而ZGC这里是之前持有的指针由于GC后失效了 , 需要通过读屏障修正指针 。
?
后面3行代码都不需要加读屏障:Object p = o这行代码并没有从堆中读取数据;o.doSomething()也没有从堆中读取数据;obj.fieldB不是对象引用 , 而是原子类型 。
正是因为Load Barriers的存在 , 所以会导致配置ZGC的应用的吞吐量会变低 。 官方的测试数据是需要多出额外4%的开销:
那么 , 判断对象是Bad Color还是Good Color的依据是什么呢?就是根据上一段提到的Colored Pointers的4个颜色位 。 当加上读屏障时 , 根据对象指针中这4位的信息 , 就能知道当前对象是Bad/Good Color了 。
?
「扩展阅读」:既然低42位指针可以支持4T内存 , 那么能否通过预约更多位给对象地址来达到支持更大内存的目的呢?答案肯定是不可以 。 因为目前主板地址总线最宽只有48bit , 4位是颜色位 , 就只剩44位了 , 所以受限于目前的硬件 , ZGC最大只能支持16T的内存 , JDK13就把最大支持的内存从4T扩大到了16T 。
?
ZGC tuning启用ZGC比较简单 , 设置JVM参数即可:-XX:+UnlockExperimentalVMOptions 「-XX:+UseZGC」 。 调优也并不难 , 因为ZGC调优参数并不多 , 远不像CMS那么复杂 。 它和G1一样 , 可以调优的参数都比较少 , 大部分工作JVM能很好的自动完成 。 下图所示是ZGC可以调优的参数:


推荐阅读