ZGC原理与实现分析
ZGC: 可扩展的低延迟的垃圾回收器
目标
支持TB级堆内存(最大4T)
最大GC停顿10ms
对吞吐量影响最大不超过15%
数据
SPECjbb 2015基准测试,128G堆内存,单次GC停顿最大1.68ms, 平均1.09ms
特性
Colored pointer
在对象的引用中借用几个bit存储额外状态标记,Load Barrier会根据这些状态标记执行不同的逻辑
Load Barrier
加载屏障:在应用线程从堆中加载对象应用后,执行的一段逻辑
跟CPU中的内存屏障(Memory barrier)完全没有关联
Single generation
目前ZGC没有分代,每次GC都会标记整个堆
Page Allocation
将堆分为 2M(small), 32M(medium), n*2M(large)三种大小的页面(Page)来管理,根据对象的大小来判断在那种页面分配
Partial compaction
在relocation阶段将Page中活的对象转移到另一个Page,并整个回收原Page。会根据一定算法选择部分Page进行整理。
Mostly Concurrent
大部分对象标记和对象转移都是可以和应用线程并发。只会在以下阶段会发生stop-the-world
1. GC开始时对root set的标记时
2. 在标记结束的时候,由于并发的原因,需要确认所有对象已完成遍历,需要进行暂停
3. 在relocate root-set 中的对象时
原理
简述
逻辑上一次ZGC分为Mark(标记)、Relocate(迁移)、Remap(重映射)三个阶段
Mark: 所有活的对象都被记录在对应Page的Livemap(活对象表,bitmap实现)中,以及对象的Reference(引用)都改成已标记(Marked0或Marked1)状态
Relocate: 根据页面中活对象占用的大小选出的一组Page,将其中中的活对象都复制到新的Page, 并在额外的forward table(转移表)中记录对象原地址和新地址对应关系
Remap: 所有Relocated的活对象的引用都重新指向了新的正确的地址
实现上,由于想要将所有引用都修正过来需要跟Mark阶段一样遍历整个对象图,所以这次的Remap会与下一次的Remark阶段合并。
所以在GC的实现上是2个阶段,即Mark&Remap阶段和Relocate阶段
向下箭头表示STW, 横向箭表示并发阶段
Colored pointer
在64位系统中,ZGC利用了对象引用的4bit(低42位:对象的实际地址)
Marked0/marked1: 判断对象是否已标记
Remapped: 判断应用是否已指向新的地址
Finalizable: 判断对象是否只能被Finalizer访问(本文分析忽略此标记)
这几个bits在不同的状态也就代表这个引用的不同颜色
为什么有2个mark标记?
每一个GC周期开始时,会交换使用的标记位,使上次GC周期中修正的已标记状态失效,所有引用都变成未标记。
GC周期1:使用mark0, 则周期结束所有引用mark标记都会成为01。
GC周期2:使用mark1, 则期待的mark标记10,所有引用都能被重新标记。
内存映射
通过Linux系统调用mmap将标记位(001,010,100)三种地址空间映射到同一地址上,使三种地址解析后都指向同一地址,Load Barrer保证返回的地址是其中一个。
GC周期
GC在每个阶段维护一个全局的唯一的期望标记,当发现引用的状态跟期望的不一致,Load barrier会修复应用的标记到期待的状态。并会根据状态的不同执行不同的逻辑。
下面分析不同阶段的实现流程
当前为Mark/Remap阶段
期待的标记值为001,此处只关注Mark操作,Remap逻辑下面说明。
当前加载的引用标记010,Load barrier会将引用的标记修正为001,然后保存回这个引用的来源对象中,这样在下次再加载相同时可以避免重复执行。同时会帮助GC进行对象标记,方式为将这个引用添加到当前线程的本地标记stack中,并发的GC线程会遍历这些引用,并递归遍历引用的对象图
当前为Relocation阶段
期待的标记值为100
GC线程会执行为relocation set执行relocate工作,将page编辑为relocating(迁移中),只迁移对象,不关注对象的引用,relocation结束后,对象的引用会指向过期的位置。
此阶段业务线程加载对象引用时,进行remap操作:先判断指向的页面状态是否为relocating, 如果是relocating, 会协助GC线程做relocate工作。并更新此引用的的标记为100,如果不是relocating,直接更新标记为100。
当Relocation阶段完成时会存在部分引用未更新,标记为001。
来到下一次GC周期:
当前为Mark/Remap阶段
期待的标记值为010
如果当前加载的引用为100,表示已完成remap,更新标记为010
如果为其他状态,则会执行rmap操作,然后更新标记为010
同时会对对象进行mark操作,前面已经说明。
如此反复切换。
Page管理
对象分配
我们知道在一些GC算法下分配对象是通过撞指针法,也即是TLAB机制来分配。在ZGC中针对不同类型的Page,有不同的分配机制。
在堆上分配对象时,是根据对象的大小选择在不同类型的Page中分配,不同Page对象的分配策略不同。
Small Page(<=256K):每个CPU会关联一个small page,线程在分配对象时,先查找线程所运行在的cpu id, 找到关联的Page,进行分配。page剩余内存不够时,会尝试在新Page分配并切换cpu绑定的page为新的page。
Medium Page(<=4M): 所有线程在同一个Page分配
Large Page:每个large对象占用一个Page, 根据对象大小先分配合适大小的Page,然后在Page中分配对象
ZGC触发时机
ZGC目前有4中机制触发GC
1. 定时触发,默认为不使用,可通过ZCollectionInterval参数配置
2. 预热触发,最多三次,在堆内存达到10%、20%、30%时触发,主要时统计GC时间,为其他GC机制使用
3. 分配速率,基于正态分布统计,计算内存99.9%可能的最大分配速率,以及此速率下内存将要耗尽的时间点,在耗尽之前触发GC(耗尽时间 - 一次GC最大持续时间 - 一次GC检测周期时间)
4. 主动触发,(默认开启,可通过ZProactive参数配置) 距上次GC堆内存增长10%,或超过5分钟时,对比距上次GC的间隔时间跟(49 * 一次GC的最大持续时间),超过则触发
简单的GC示例
第一次STW, 标记roots对象
并发标记阶段,所有活对象以及对象引用都被标记
此后会有第二次STW,确保所有对象都被标记
选择需要整理的Page集合(relocation set)
第三次STW, 转移root中的对象
当一个Page内的活对象全部转移后,此Page的内存可以立即重用。
这是个和有用的特性,relocation set中下个page的对象可以转移到这个释放的内存中,理论上在GC时只需要有一个可转移的空页就可以了。
到此,一个GC周期就结束了。
剩下的修复工作由Load Barrier以及下次GC来完成
转载请注明来源:https://www.jianshu.com/p/4e4fd0dd5d25
参考
https://www.zhihu.com/question/287945354/answer/458761494
http://dinfuehr.github.io/blog/a-first-look-into-zgc/
http://cr.openjdk.java.net/~pliden/slides/ZGC-Jfokus-2018.pdf
https://www.usenix.org/legacy/events/vee05/full_papers/p46-click.pdf
http://go.azul.com/continuously-concurrent-compacting-collector