ZGC 源码分析 转载

时间:2024-04-01 22:45:30

https://hg.openjdk.java.net/jdk/jdk12/file/06222165c35f/src/hotspot/share/gc

ZGC 源码分析 转载

 

3.2 ZGC的特性
并发
由于停顿时间小于10ms,显而易见的,回收周期绝大部分逻辑将于Mutator并发执行。

基于区域
与G1类似,JAVA Heap被划分为多个Region,但ZGC中Region大小并不唯一。

压缩算法
为了避免内存碎片,需要在回收周期中进行压缩。

支持NUMA
作为下一代垃圾回收算法,支持NUMA是必须的。

使用colored pointers
CMS和G1标记的是对象,而ZGC标记的是对象指针。
 

ZGC 源码分析 转载

回收过程

开始标记(STW),标记GC roots
并发的遍历堆中对象,描绘出引用关系
结束标记(STW),记录活动对象和需要回收的对象
并发的relocate准备阶段,弱引用和软引用的处理,类的卸载,relocate集合的选择等
开始relocate(STW),relocate GC roots
并发relocate,这是GC线程和Mutator线程的读屏障都可进行relocate
 

ZGC 源码分析 转载

 缺点
不支持分代回收
由于ZGC尚出于实验阶段,且分代代码代码实现较困难,目前ZGC并没有区分新生代和老年代。
因为没有分代,每次回收都会试图并发标记整个JAVA Heap,并发标记过程可能达到分钟级,如果对象分配速率很高的话,在此期间就可能分配出大量对象,导致ZGC处理困难。
后续ZGC会推出分代机制或Thread Local GC修复此问题。
ZGC 源码分析 转载

  • 不支持指针压缩
    由于引入了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触发策略

ZGC 源码分析 转载

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中

ZGC 源码分析 转载

  • 遍历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启动请求