java

ZGC原理与实现分析

2019-01-20  本文已影响0人  xiaozhukk

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

https://www.jianshu.com/p/5f9095ccc11e

上一篇下一篇

猜你喜欢

热点阅读