ZGC笔记
ZGC背景
对于Java项目来说,JVM在进行垃圾回收会有一个很大的问题,就是STW.
STW:Stop The World. JVM在进行STW 时,会导致业务系统暂停.
垃圾回收器的发展
为了满足不同业务场景,Java的GC算法也在不断迭代.目前推出了不同版本的GC垃圾回收器.
不同的GC垃圾回收器如图所示:
2018年,推出了Jdk11,ZGC(The Z Garbage Collector)应运而生,ZGC是JDK11推出的一款追求极致的垃圾收集器.
包含的特点:
1.**停顿时间不超过10ms**(jdk16达到不超过1ms);
2.停顿时间不会随着对的大小 或者 活跃对象的大小而增加;
3.支持 8M~4TB大小的堆空间,jdk15之后支持16TB的堆空间大小;
ZGC的内存布局
为了细粒度地对内存空间进行控制,ZGC将内存空间划分成了众多小分区,称为页面(page)
ZGC中没有分代概念(新生代,老年代)
ZGC中包含3中页面.(1小页面|2中页面|3大页面)
ZGC对象分配规则:
1.当对象大小 < 256kb 时, 分配到小页面中;
2.当对象大小 在[256kb,4M]范围时,分配到中页面中;
3.当对象大小 >4M时,分配到大页面中;
设计初衷
标准大页(huge page)是Linux Kernel 2.6引入的,目的是通过使用大页内存来取代传统的4KB内存页面,以适应越来越大的系统内存,让操作系统可以支持现代硬件架构的大页面容量功能。
Huge pages 有两种格式大小: 2MB 和 1GB , 2MB 页块大小适合用于 GB 大小的内存, 1GB 页块大小适合用于 TB 级别的内存; 2MB 是默认的页大小
NUMA
在过去,对于X86架构的计算机,内存控制器还没有整合进CPU,所有对内存的访问都需要通过北桥芯片来完成。X86系统中的所有内存都可以通过CPU进行同等访问。任何CPU访问任何内存的速度是一致的,不必考虑不同内存地址之间的差异,这称为“统一内存访问”(Uniform Memory Access,UMA)。
ZGC是支持NUMA的,在进行小页面分配时会优先从本地内存分配,当不能分配时才会从远端的内存分配。对于中页面和大页面的分配,ZGC并没有要求从本地内存分配,而是直接交给操作系统,由操作系统找到一块能满足ZGC页面的空间。ZGC这样设计的目的在于,对于小页面,存放的都是小对象,从本地内存分配速度很快,且不会造成内存使用的不平衡,而中页面和大页面因为需要的空间大,如果也优先从本地内存分配,极易造成内存使用不均衡,反而影响性能。
UMA,NUMA系统的架构示意图如图所示。
ZGC核心
颜色指针(Color Pointers)
颜色指针是ZGC的核心概念.因为在指针中使用了其中几位,所以必须要求在64位机器上才能工作,且不支持指针压缩.
ZGC中低42位表示使用的堆空间位置.
ZGC高位用于GC相关操作(如 并发标记,标记转移和重定位)
ZGC的垃圾回收流程
一次ZGC流程
1.标记阶段(标记垃圾对象);
初始标记(会进行STW) : 从根节点出发,找到根节点的直接引用的对象,根对象;
并发标记 : 根据初始标记找到的根对象,使用深度优先遍历对象的成员变量进行标记;
再标记(会进行STW) : 用于处理漏标对象,STAB算法解决;
2.转移阶段(对象复制或者对象转移)
并发转移准备 : 分析有价值的GC分页(无STW)
初始转移(会进行STW) : 转移初始标记的存活对象同时做对象重定位(有STW)
并发转移 : 对转移并发标记的存活对象做转移(无STW),在并发转移阶段,会使用到转发表(类似HashMap)
0.初始阶段
在ZGC初始化后,地址的状态为Remapped,程序正常运行,在内存中分配对象,满足一定条件之后启动垃圾回收. 新创建的对象标记为Remapped.
1.初始标记
该阶段需要暂停(STW),初始标记只需要扫描所有GC Roots ,其处理时间和GC Roots的数量成正比,停顿时间不会随着堆的大小或者活跃对象的大小而增加.
2.并发标记
这个阶段不需要暂停(没有STW),扫描剩余的所有对象,这个处理时间比较长,所以走并发,业务线程与GC线程同时运行。但是这个阶段会产生漏标问题。
3.再标记
这个阶段需要暂停(没有STW),主要处理漏标对象,通过SATB算法解决和G1中的解决漏标的方法相同.
4.并发转移准备
这个阶段(无STW)通过可达性分析算法 对内存分页进行分析. 分析回收效率最高的分页,进行垃圾对象回收.
5.初始转移
这个阶段(有STW)会对标记为存活的对象 进行初始转移.并对存活对象进行重定位.
6.并发转移
这个阶段(无STW)会对 存活对象进行转移.通过 标记复制或者 标记转移 方式 进行垃圾回收.
并发转移内容
在并发转移中 涉及不同的页面,以及转发表.
转发表 : 类似HashMap 存储了存活对象进行转移前后的地址,用于标记复制/标记整理
之后的对象引用寻址.
在进行并发转移时,地址中的标记位 由Remapped(蓝色)为M0(绿色),此时对象从小页面1转移到小页面2.之后会将 对象B,C的新旧地址存入转发表中.而对象A属于GC Roots标记的根对象,所以在转发表中无记录.
在GC完成之后.如果有其他对象引用到对象B,C.那么就会到转发表中查询新旧地址.并更新引用地址.
在更新完地址内容之后,会将转发表中的数据进行删除.
可达性分析算法
可达性分析算法.用于判断对象是否存活.通过GC Roots 的对象作为起始点,通过这些对象调用的引用链(Reference Chain).当一个对象在GC Roots中没有任何引用链时,该对象不可用.
属于GC Roots的对象
虚拟机栈(栈帧中的本地变量表) : 各个线程调用方法堆栈中使用到的参数、局部变量、临时变量等;
方法区中的类静态变量 : java类的引用类型静态变量;
方法区中的常量 : 如,字符串常量池中的引用;
本地方法栈中的JNI指针 : Java中使用 native 修饰的方法,调用c++源码中的方法.
基于颜色指针的重定位
在第一次GC时.有一些对象没有被立即使用.这些处于休眠状态对象,在第二次GC时,会在标记阶段的并发标记过程中,进行对象重定位,更新相关的引用地址.
这些存活的对象的指针会从M0(绿色) 变成M1(红色).
在进行重定位时,会使用到读屏障的方式来进行重定位.
读屏障
涉及对象:并发转移但还没做对象重定位的对象(着色指针使用M0和M1可以区分)
触发时机:在两次GC之间业务线程访问这样的对象
触发操作:对象重定位+删除转发表记录(两个一起做原子操作)
读屏障是JVM向应用代码插入一小段代码的技术。当应用线程从堆中读取对象引用时,就会执行这段代码。
需要注意的是,仅“从堆中读取对象引用”才会触发这段代码。
ZGC 的参数设置
ZGC的优点不仅仅是STW停顿的时间短,还在于配置参数也相对简单,在服务环境中能够自适应.在极端环境中需要对ZGC中的参数进行调整.
1.堆空间大小调整 Xmx. 当分配速率过高,超过回收效率时,堆内存空间不足,会触发Allocation Stall .会减缓当前的用户线程. 在GC 日志中如果看到 AllocationStall 通常可以认为是 堆空间较小或者 concurrent gc threads数偏小.就需要对Xmx参数进行调整.
2.GC触发时机 ZAllocationSpikeTolerance ,ZCollectionInterval. ZAllocationSpikeTolerance 用于估算当前的堆内存分配效率.在当前剩余的堆内存下.ZAllocationSpikeTolerance越大,估算达到OOM的时间越快,ZGC就会更早地进行GC.ZCollectionInterval 用来指定 GC 发生的间隔,以秒为单位触发 GC.
3.GC线程 ParallelGCThreads , ConcGCThreads. ParallelGCThreads 是设置STW任务的GC线程数,默认为CPU个数的60%. ConcGCThreads 是并发阶段GC线程的数量,默认为CPU个数的12.5%. 增加GC线程数量,可以加快GC完成,减少各个阶段的时间,但是会增加CPU的开销.
ZGC的应用场景
从性能角度来分析,不同的配置对性能的影响不同.如果内存的空间足够大时,ZGC在各类Benchmark(基准)中超过G1约 5%~20% .在堆空间比较小时,则是低于G1 约 10% . 不同的配置对于应用的影响也不相同.需要根据实际场景来进行判断.
因为ZGC 不支持 指针压缩 和 分代年龄. 内存占用比G1要大,在小堆场景中比较明显,而大堆场景中 占用的内存则不那么明显. 所以可以 使用ZGC来提升业务中用户体验的场景有::
1.超大堆应用。超大堆(百 G 以上)下,CMS 或者 G1 如果发生 Full GC,停顿会在分钟级别,可能会造成业务的终端,强烈推荐使用 ZGC。
2.当业务应用需要提供高服务级别协议(Service Level Agreement,SLA),例如 99.99% 的响应时间不能超过 100ms,此类应用无论堆大小,均推荐采用低停顿的 ZGC。
ZGC注意事项
RSS 内存异常现象
由ZGC的垃圾回收过程可知,ZGC使用多映射 multi-mapping 的方法实现了三份虚拟内存同事指向同一份物理内存. 但在Linux 统计进行的RSS 内存占用的算法比较脆弱,这种多映射的方法没有考虑完整.所以,根据当前Linux采用大小页时,Linux 统计的ZGC 的Java进程的内存表现是不同的. 在内核使用小页的Linux版本中,这三映射同一个物理内存 会被Linux 的RSS占用算法统计3次.所以,通常可以看到使用ZGC的Java进程的RSS内存膨胀了3倍左右.但使用只占用数据的 1 3 {1}\over{3} 31.这样会对运维或者其他业务造成困扰. 因此,在内核使用大页的Linux版本,这部分三映射的物理内存会统计到hugetlbfs inode上,而不是当前Java进程上.
共享内存调整
ZGC需要再share memory中建立一个内存文件来作为实际物理内存占用.所以,当要使用的Java堆的大小 > /dev/shm 的大小时,需要对 /dev/shm 的大小进行调整. 命令如下(下面是将 /dev/shm 调整为 64G):
vi/etc/fstabtmpfs /dev/shm tmpfs defaults,size= 65536M00
mmap 节点上限调整
ZGC 的堆申请和传统的 GC 有所不同,需要占用的 memory mapping 数目更多,即每个 ZPage 需要 mmap 映射三次,这样系统中仅 Java Heap 所占用的 mmap 个数为 (Xmx / zpage_size)3,默认情况下 zpage_size 的大小为 2M.
为了给 JNI 等 native 模块中的 mmap 映射数目留出空间,内存映射的数目应该调整为 (Xmx / zpage_size) 31.2.
默认的系统 memory mapping 数目由文件 /proc/sys/vm/max_map_count 指定,通常数目为 65536,当给 JVM 配置一个很大的堆时,需要调整该文件的配置,使得其大于 (Xmx / zpage_size) 3*1.2