JDK11的ZGC - 学习笔记

更多 Java 虚拟机方面的文章,请参见文集《Java 虚拟机》


看到一篇文章,做一点记录:Java程序员的荣光,听R大论JDK11的ZGC

ZGC的成绩是,无论你开了多大的堆内存(1288G? 2T?),硬是能保证低于10毫秒的JVM停顿,远胜前代的G1。

 

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

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

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

说几乎,就是还有三个非常短暂的STW的阶段,所以ZGC并不是Zero Pause GC啦。比如开始的Pause Mark Start阶段,要做根集合(root set)扫描,包括全局变量啊、线程栈啊啥的里面的对象指针,但不包括GC堆里的对象指针,所以这个暂停就不会随着GC堆的大小而变化(不过会根据线程的多少啊、线程栈的大小之类的而变化)”。

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

Colored Pointer 从64位的指针中,借了几位出来表示 Finalizable、Remapped、Marked1、Marked0。 所以它不支持32位指针也不支持压缩指针, 且堆的上限是4TB。

 

有Load barrier在,就会在不同阶段,根据指针颜色看看要不要做些特别的事情(Slow Path)。

三、像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能更好的处理大对象的分配。

四、和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。

 

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

 

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

 

活的对象都移走之后,这个region可以立即释放掉,并且用来当作下一个要扫描的region的to region。所以理论上要收集整个堆,只需要有一个空region就OK了。

4. Remap - 修正指针
最后将指针都妥帖地更新指向新地址。上一个阶段的Remap,和下一个阶段的Mark是混搭在一起完成的,这样非常高效,省却了重复遍历对象图的开销。”

 

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

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

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

这里还需要使用Write Barrier(写屏障)技术,G1在平时写引用时,GC移动对象时,都要同步去更新RememberSet,跟踪跨代跨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同志在访问,所以就快了。

七、并行

在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唯一的弱点了。
分代原本是因为most object die young的假设,而让新生代和老生代使用不同的GC算法。

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



作者:专职跑龙套
链接:https://www.jianshu.com/p/60d9e125dcf3
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值