41

Java 程序员的荣光,听 R 大论 JDK 11 的 ZGC

 5 years ago
source link: https://mp.weixin.qq.com/s/KUCs_BJUNfMMCO1T3_WAjw?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

ZGC来了 !!! Java程序员可以光荣的远离讨厌的GC停顿和调优了。ZGC的成绩是,无论你开了多大的堆内存(1288G? 2T?),硬是能保证低于10毫秒的JVM停顿。

SPECjbb 2015基准测试,在128G的大堆下,最大停顿时间才  1.68ms  (不是平均,不是90%,99%,是Max ! ),远低于最初的目标-那保守的10ms,也远胜前代的G1。

MbmU7nu.jpg!web

大家的第一反应都是这么颠覆性的东西怎么来的,G1 通过每次只回收部分Region而不是全堆,改善了大堆下的停顿时间,但在普通大小的堆里表现并没惊喜,现在怎么突然就翻天了,一点心理准备都没有啊。

如果文章太长不想看下去,你只要记住R大下面这句话就够了:

与标记对象的传统算法相比,ZGC在指针上做标记,在访问指针时加入Load Barrier(读屏障),比如当对象正被GC移动,指针上的颜色就会不对,这个屏障就会先把指针更新为有效地址再返回,也就是,永远只有单个对象读取时有概率被减速,而不存在为了保持应用与GC一致而粗暴整体的Stop The World。

其实Azul JDK的皇牌 C4 垃圾收集 ,早就同样以最高十毫秒停顿成为江湖传说。 曾在Azul的R大, 看着JDK11 ZGC的算法和结果倍感熟悉,与ZGC的领队Per Liden大大聊完之后,确认了 ZGC Azul Pauseless GC,是, 等,价,的。(R大御览本文时 -  其他同学是预览,R大是御览,想半天,选定了“等价”这个字眼 )

EVz6ri7.jpg!web

(R大拍的Per大大在JVMLS)

嗯, 如果你还有空,下面让我们来继续聊聊ZGC的八大特征。

一、所有阶段几乎都是并发执行的

这里的并发(Concurrent),说的是应用线程与GC线程齐头并进,互不添堵。

说几乎,就是还有三个非常短暂的STW的阶段,所以ZGC并不是Zero Pause GC啦。

R大:“比如开始的Pause Mark Start阶段,要做根集合(root set)扫描,包括全局变量啊、线程栈啊啥的里面的对象指针,但不包括GC堆里的对象指针,所以这个暂停就不会随着GC堆的大小而变化(不过会根据线程的多少啊、线程栈的大小之类的而变化)”   -- 因此ZGC可以拍胸脯,无论堆多大停顿都小于10ms。

二、并发执行的保证机制,就是Colored Pointer 和 Load Barrier

原理前面R大一句话已经说完了。Colored Pointer 从64位的指针中,借了几位出来表示Finalizable、Remapped、Marked1、Marked0。 所以它不支持32位指针也不支持压缩指针, 且堆的上限是4TB。

YreUvei.jpg!web

有Load barrier在,就会在不同阶段,根据指针颜色看看要不要做些特别的事情(Slow Path)。注意下图里只有第一种语句需要读屏障,后面三种都不需要,比如值是原始类型的时候。

2qiYNzi.jpg!web

R大还提到了ZGC的Load Value Barrier,与Red Hat的Shenandoah收集器的不同,后者选择了70年代的比较基础的Brooks Pointer ,而前者在也是很老的Baker barrier上加入了self healing的特性,比如下面的代码:

Object a = obj.x;

Object b = obj.x;

两行代码都插入了读屏障,但ZGC在第一个读屏障之后,不但a的值是新的,self healing下obj.x的值自身也会修正,第二个读屏障时就直接进入FastPath,没有消耗了; 而Shenandoah 则不会修正obj.x的值,第二个读屏障又要SlowPath一次。

三、像G1一样划分Region,但更加灵活

ZGC将堆划分为Region作为清理,移动,以及并行GC线程工作分配的单位。

不过G1一开始就把堆划分成固定大小的Region,而ZGC 可以有2MB,32MB,N× 2MB 三种Size Groups,动态地创建和销毁Region,动态地决定Region的大小。

256k以下的对象分配在Small Page, 4M以下对象在Medium Page,以上在Large Page。

所以ZGC能更好的处理大对象的分配。

iiABRfI.png!web

四、和G1一样会做Compacting-压缩

CMS是Mark-Sweep标记过期对象后原地回收,这样就会造成内存碎片,越来越难以找到连续的空间,直到发生Full GC才进行压缩整理。

ZGC是Mark-Compact ,会将活着的对象都移动到另一个Region,整个回收掉原来的Region。

而G1 是 incremental copying collector,一样会做压缩。

下面粗略了几十倍地过一波回收流程,小阶段都被略过了哈:

1. Pause Mark Start -初始停顿标记

停顿JVM地标记Root对象,1,2,4三个被标为live。

2MBb6ja.jpg!web

2. Concurrent Mark -并发标记

并发地递归标记其他对象,5和8也被标记为live。

uaUvAbu.jpg!web

3. Relocate - 移动对象

对比发现3、6、7是过期对象,也就是中间的两个灰色region需要被压缩清理,所以陆续将4、5、8  对象移动到最右边的新Region。移动过程中,有个forward table纪录这种转向。

N7vuyqy.jpg!web

R大这里又赞扬了一下C4/ZGC的Quick Release特性:活的对象都移走之后,这个region可以立即释放掉,并且用来当作下一个要扫描的region的to region。所以理论上要收集整个堆,只需要有一个空region就OK了。

而RedHat的Shenandoah 因为它的forward pointer的设计,则需要有1/2个Heap是空的。

4. Remap - 修正指针

最后将指针都妥帖地更新指向新地址。这里R大还提到一个亮点: “上一个阶段的Remap,和下一个阶段的Mark是混搭在一起完成的,这样非常高效,省却了重复遍历对象图的开销。”

NRjqIb2.jpg!web

五、没有G1占内存的Remember Set,没有Write Barrier的开销

G1 保证“每次GC停顿时间不会过长”的方式,是“每次只清理一部分而不是全部的Region”的增量式清理。

那独立清理某个Region时 , 就需要有RememberSet来记录Region之间的对象引用关系, 这样就能依赖它来辅助计算对象的存活性而不用扫描全堆, RS通常占了整个Heap的20%或更高。

这里还需要使用Write Barrier(写屏障)技术,G1在平时写引用时,GC移动对象时,都要同步去更新RememberSe,跟踪跨代跨Region间的引用,特别的重。而CMS里只有新老生代间的CardTable,要轻很多。

ZGC几乎没有停顿,所以划分Region并不是为了增量回收,每次都会对所有Region进行回收,所以也就不需要这个占内存的RememberSet了,又因为它暂时连分代都还没实现,所以完全没有Write Barrier。

六、支持Numa架构

现在多CPU插槽的服务器都是Numa架构了,比如两颗CPU插槽(24核),64G内存的服务器,那其中一颗CPU上的12个核,访问从属于它的32G本地内存,要比访问另外32G远端内存要快得多。

JDK的 Parallel Scavenger 算法支持Numa架构,在SPEC JBB 2005 基准测试里获得40%的提升。

原理嘛,就是申请堆内存时,对每个Numa Node的内存都申请一些,当一条线程分配对象时,根据当前是哪个CPU在运行的,就在靠近这个CPU的内存中分配,这条线程继续往下走,通常会重新访问这个对象,而且如果线程还没被切换出去,就还是这位CPU同志在访问,所以就快了。

但可惜CMS,G1不支持Numa,现在ZGC 又重新做了简单支持,哈哈哈。

R大补充,G1也打算支持了Numa了: http://openjdk.java.net/jeps/157

七、并行

在ZGC 官网上有介绍,前面基准测试中的32核服务器,128G堆的场景下,它的配置是:

20条ParallelGCThreads,在那三个极短的STW阶段并行的干活 -  mark roots, weak root processing(StringTable, JNI Weak Handles,etc)和 relocate roots ;

4条ConcGCThreads,在其他阶段与应用并发地干活 - Mark,Process Reference,Relocate。 仅仅四条,高风亮节地尽量不与应用争抢CPU 。

ConcCGCThreads开始时各自忙着自己平均分配下来的Region,如果有线程先忙完了,会尝试“偷”其他线程还没做的Region来干活,非常勤奋。

八、单代

没分代,应该是ZGC唯一的弱点了。所以R大说ZGC的水平,处于AZul早期的PauselessGC  与 分代的C4算法之间 - C4在代码里就叫GPGC,Generational Pauseless GC。

分代原本是因为most object die young的假设,而让新生代和老生代使用不同的GC算法。但C4已经是全程并发算法了,为什么还要分代呢?

R大说:

“因为分代的C4能承受的对象分配速度(Allocation Rate), 大概是原始PGC的10倍。

如果对整个堆做一个完整并发收集周期,持续的时间可能很长比如几分钟,而此期间新创建的对象,大致上只能当作活对象来处理,即使它们在这周期里其实早就死掉可以被收集了。如果有分代算法,新生对象都在一个专门的区域创建,专门针对这个区域的收集能更频繁更快,意外留活的对象更也少。

而Per大大因为分代实现起来麻烦,就先实现出比较简单可用的单代版本。所以ZGC如果遇上非常高的对象分配速率,目前唯一有效的“调优”方式就是增大整个GC堆的大小来让ZGC有更大的喘息空间。”

小结

ZGC这么让Java有面子有期待的事情,不转不是Java人 !!!

全程各种R大聊天实录,不转不是R大粉!!!!

小结2

歇了一年多后的再次更新,因为错过了公众号最黄金的时代,麻烦大家重新关注下本号,给深夜码字的作者一点慰籍。

各位老大写公众号推荐集合时,求顺带捎上小号。

R7Rj6nV.jpg!web

参考资料

1. ZGC wiki:

https://wiki.openjdk.java.net/display/zgc/Main

2. R大的知乎回答:

https://www.zhihu.com/question/287945354/answer/458761494

3. ZGC回收器到底有多变态? by 贺卓凡     ImportSource  

本文图片多有借用,感谢。链接太长不好贴,大家按标题搜索。

4. A FIRST LOOK INTO ZGC:

http://dinfuehr.github.io/blog/a-first-look-into-zgc/

5. AZul的《The Pauseless GC Algorithm》论文:

https://www.usenix.org/legacy/events/vee05/full_papers/p46-click.pdf

6. AZul开源的C4参考实现,原汁原味的论文实现

https://github.com/GregBowyer/ManagedRuntimeInitiative/tree/master/MRI-J/hotspot/src/azshare/vm/

fENNFb7.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK