https://hg.openjdk.java.net/jdk/jdk12/file/06222165c35f/src/hotspot/share/gc
3.2 ZGC的特性
并发
由于停顿时间小于10ms,显而易见的,回收周期绝大部分逻辑将于Mutator并发执行。
基于区域
与G1类似,JAVA Heap被划分为多个Region,但ZGC中Region大小并不唯一。
压缩算法
为了避免内存碎片,需要在回收周期中进行压缩。
支持NUMA
作为下一代垃圾回收算法,支持NUMA是必须的。
使用colored pointers
CMS和G1标记的是对象,而ZGC标记的是对象指针。
回收过程
开始标记(STW),标记GC roots
并发的遍历堆中对象,描绘出引用关系
结束标记(STW),记录活动对象和需要回收的对象
并发的relocate准备阶段,弱引用和软引用的处理,类的卸载,relocate集合的选择等
开始relocate(STW),relocate GC roots
并发relocate,这是GC线程和Mutator线程的读屏障都可进行relocate
缺点
不支持分代回收
由于ZGC尚出于实验阶段,且分代代码代码实现较困难,目前ZGC并没有区分新生代和老年代。
因为没有分代,每次回收都会试图并发标记整个JAVA Heap,并发标记过程可能达到分钟级,如果对象分配速率很高的话,在此期间就可能分配出大量对象,导致ZGC处理困难。
后续ZGC会推出分代机制或Thread Local GC修复此问题。
-
不支持指针压缩
由于引入了colored pointer机制,不再支持指针压缩。而指针压缩对于32GB以下堆有着显著的性能和空间利用率提升。 -
支持操作系统很少
仅支持Linux/x64,JDK 13中将支持Linux/AArch64
触发GC的时机
ZGC提供四种策略,其中一种满足条件即触发回收:
rule_timer,定时策略,距离上次GC时间超过interval即触发GC
rule_warmup,VM启动后,如果从来没有发生过GC,则在堆内存使用超过10%、20%、30%时,分别触发一次GC,以收集GC数据
rule_allocation_rate,根据对象分配速率决定是否GC
rule_proactive,主动控制策略,根据距离上次GC增长的堆空间和距离上次GC的时间判断是否触发GC
如果对象分配过快,以至于以上四种策略均无法及时回收对象,则在到达阈值后,STW并行回收。
GC触发策略
rule_warmup
- 先判断系统是否已经预热,如果GC次数大于等于3,则不使用rule_warmup策略
- 每当堆空间占用率大于10%、20%、30%时,触发一次GC
rule_allocation_rate
如果VM启动后,从来发生过GC,则不使用rule_allocation_rate
ZGC分配速率的计算与G1不同,采用的是正态分布,置信度为99.9%时,最大内存分配速率为((ZStatAllocRate::avg() * 1) + (ZStatAllocRate::avg_sd() * 3.290527))
ZAllocationSpikeTolerance是个修正参数,默认2,加入该修正系数后,置信度远大于99.9%,计算公式为((ZStatAllocRate::avg() * ZAllocationSpikeTolerance) + (ZStatAllocRate::avg_sd() * 3.290527))
根据最大分配速率,可以计算出到达OOM的剩余时间
同样根据正态分布,取置信度99.9%,计算出最大GC持续时间
当time_until_oom - max_duration_of_gc - sample_interval小于0时,即触发GC
rule_proactive
先判断JVM参数ZProactive(默认true),如果是false则不使用rule_proactive
如果VM还没有预热,则不使用rule_proactive
如果距离上次GC,堆内存占用增长小于10%且小于5分钟,则不使用rule_proactive
如果距离上次GC时间超过最大预测GC时间乘49,则触发GC
内存管理
ZGC与传统GC不同,标记阶段标记的是指针(colored pointer),而非传统GC算法中的标记对象。ZGC借助内存映射,将多个地址映射到同一个内存文件描述符上,使得ZGC回收周期各阶段能够使用不同的地址访问同一对象
- ZGC使用41-0位记录对象地址,42位地址为应用程序提供了理论4TB的堆空间
- 45-42位为metadata比特位, 对应于如下状态: finalizable,remapped,marked1和marked0
- 46位为保留位,固定为0
- 63-47位固定为0
内存映射之前需要创建文件描述符,名称为java_heap
- 优先创建内存文件描述符
- 通过syscall函数调用memfd_create创建文件描述符
2.3 分级管理
与操作系统类型,为了便于管理,ZGC也定义了两级结构:虚拟内存和物理内存,分别由ZVirtualMemoryManager和ZPhysicalMemoryManager管理。
ZGC的物理内存并不是通常意义上的物理内存,而仅表示mutator使用的Java heap内存。因此仍然是用户空间虚拟内存,仅为了与Mark0、Mark1、Remapped视图区分。本文中的物理内存,如无特殊说明,特指ZGC中的物理内存。
- 物理内存以segment组织,每次分配既分配一个新的segment
- 调用ZMemoryManager alloc_from_front从free list中分配内存area
- 释放物理内存时,与free list前后area比较,如果start或end相等,则合并area
- 否则创建新的area,加入到free list中
- 遍历free list
- 如果内存段size等于申请的size,则返回当前area,并将此area从free list移除
- 如果内存段size大于申请的size,则裁剪此area
- 如果遍历完整个free list仍然无法分配,则触发OOM
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
分页
与G1的分区类似,ZGC将堆划分成Page进行管理,不同的是ZGC中Page大小不是固定的,而是分为三种Small、Medium、Large三类。
ZGC有3种不同的页面类型:小型(2MB),中型(32MB)和大型(2MB的倍数)。
large page分配时,需要按small page size对齐,因此large page的size一定是2M的整数倍。
当对象size小于等于256K时(2M / 8),在small page分配
当对象size小于等于4M时(32M / 8),在medium page分配
当对象size大于4M时,在large page分配
- 在page中分配,如果成功则返回
- 如果失败,则调用alloc_page函数分配
- JVM参数ZStallOnOutOfMemory,控制是否当空间不足时,是否同步等待GC完成,false表示立即抛出OOM,默认为true。
- 调用ZHeap的alloc_page函数分配page
- 如果分配成功,CAS添加内存占用量
- 调用PageAllocator的alloc_page函数分配page
- 如果分配成功,则将page添加到page table中
根据JVM参数ZStallOnOutOfMemory判断,true则调用alloc_page_blocking,false则调用alloc_page_nonblocking分配page
分配成功后,判断该page是否已经map,如否,则map到marked0、marked1、remapped虚拟内存空间
- 获取heap锁,该锁为全局锁
- 调用alloc_page_common分配page
- 如果分配失败,则触发GC并等待,直到分配成功
- 首先尝试从page cache中分配,支持NUMA架构,区分local和remote内存
- 然后尝试从pre mapped中分配,预分配内存一般就是xms参数指定的内存区域
- 调用create_page从物理内存(ZPhysicalMemory)直接分配
对象分配
ZGC的对象分配流程大体与G1类似。分配流程图在JVM G1 源码分析(二)- 对象的堆空间分配内。
主要代码在src/hotspot/share/gc/shared目录下,为多个GC算法的共用代码。
ZGC定制逻辑由gc shared代码调用ZCollectedHeap、ZHeap等ZGC派生类实现。
2.1 allocate_new_tlab
当线程的TLAB可用内存不足时,MemAllocator的allocate_inside_tlab_slow会调用ZCollectedHeap的allocate_new_tlab申请新的TLAB。
分配前,先对象字节对齐,默认8字节对齐
调用ZHeap的alloc_tlab进行分配
alloc_tlab在校验size合法性后,调用ZObjectAllocator的alloc_object进行分配。
- TLAB的最大size为常量ZObjectSizeLimitSmall,即2MB
由于TLAB max size是2MB,会调用alloc_small_object进行分配。
2.2 mem_allocate
当TLAB分配失败时,MemAllocator的allocate_outside_tlab会调用ZCollectedHeap的mem_allocate进行慢分配。
size需要对象对齐,默认8字节对齐
mem_allocate调用ZHeap的alloc_object,进行对象分配。
- 调用ZObjectAllocator的alloc_object进行分配;
- 如果分配失败,则调用out_of_memory记录
根据需要分配的size大小,决定调用哪个函数进行分配。
- 当对象size小于等于256K时(2M / 8),在small page分配;
- 当对象size小于等于4M时(32M / 8),在medium page分配;
- 当对象size大于4M时,在large page分配。
- 调用ZPage alloc_object_atomic在页上分配对象
- 如果失败,则先分配新页,然后在新页上分配对象
alloc_object_atomic没有锁,而是通过自旋+CAS实现并发控制,自旋使用CAS分配对象,直到成功或Page没有足够的空间
大对象分配略有不同
- 对象size按2MB对齐
- 分配large page
- 在large page上分配对象
GC回收
GC回收周期包括如下11个子阶段:
phase 1:初始标记,需要STW
phase 2:并发标记
phase 3:标记结束,需要STW
phase 4:并发处理软引用、弱引用
phase 5:并发重置Relocation Set
phase 6:并发销毁可回收页
phase 7:内存验证
phase 8:并发选择Relocation Set
phase 9:并发准备Relocation Set
phase 10:开始Relocate,STW
phase 11:并发Relocate
- ZGC自身的触发策略都使用异步消息,包括rule_timer、rule_warmup、rule_allocation_rate、rule_proactive
- metaspace GC使用异步消息
- 其他情况使用同步消息
ZGC使用ZMessagePort类传递消息,ZMessagePort内部使用了ZList队列
同步消息逻辑如下:
- 首先构造request
- request入队
- 通知消费者
- 如果是同步消息,需要等待request处理完
消息消费者负责消费队列中的消息,如果是异步消息,则直接读取类变量_message。
2.3 开始gc cycle
ZDriver启动一个线程,死循环判断是否应该启动gc cycle
start_gc_cycle调用ZMessagePort的receive方法等待启动请求
调用run_gc_cycle方法,执行GC
执行GC后,调用end_gc_cycle,ACK启动请求