Java ZGC 工作原理/详解/模型
GC(Garbage Collector) 是JVM(Java Virtual Machine)虚拟机的所提供的重要特性,GC让开发者在使用Java进行应用开发时不再需要考虑进行手工的内存释放(Memory De-allocation),从内存管理相关的工作中释放出来,能够更多地聚焦于程序逻辑本身。 简单来说,GC的核心逻辑在于Java程序可以在运行时动态申请内存,使用完的内存不需要手工释放,而是由GC在适当的时候检测到这些可以被回收的内存并进行释放(对于C/C++语言来说这是令人惊讶的,对于这类语言来说,运行时动态申请的内存必须在使用完后进行手工释放,否则将引起内存泄漏)。Java GC经过多年的发展,已经衍生出多种的GC实现方案,不同的GC实现均包含STW(Stop the world)过程,该过程意味着所有的业务线程均需要停止工作,由GC线程负责进行内存遍历/可回收内存定位/内存压缩等一系列动作,动作结束之后,业务线程恢复工作。STW的可恶之处在于业务线程可能在任何时间点由于GC的STW被暂停,且被暂停的时间长短和应用的堆区(Heap)大小/已创建对象的多少相关。所以GC的实现方向之一就是尽量压缩STW所使用的时间,减少对业务线程的影响。 在Z GC之前,主要的GC实现STW会随着堆区大小的增大而变长,这对于一些大堆Java应用且其实时响应性要求较高的场景,GC变成影响应用性能的关键瓶颈。例如金融业务场景下的实时风控应用,一个交易请求打进去,要求100ms内就要给出对应的风控应答,如果应用本身STW就有100ms,那么无论怎样么优化应用逻辑本身,都无法避免地无法达到应答时效要求。Z GC带来两个可靠的特性:(1)单次GC的STW时间能够降稳定地控制在10ms以内(2)STW时长不会随堆区大小的增长而变长。 了解Z GC算法的实现,直接查看对应的源码实现(25KLOC源码)需要较长的时间去阅读并理清对应的工作模型。 对于开发人员来说,我们希望能够有一个简化的Z GC工作模型作为指导,能够对Z GC的实际工作的上下文有进一步的理解。查看了中文网络上的一些资料,一部分是对Z GC的简单介绍,一部分是直接的源码分析,缺少一个中间层,介绍Z GC工作模型的介绍,本文基于ACM论文<Deep Dive into ZGC: A Modern Garbage Collector in OpenJDK>,介绍Z GC的具体工作模型。文章约8500字,常规阅读时间约40分钟。 基础知识 在了解Z GC实现之前,需要一些先决的基础知识,这一部分网络上的资料较为丰富,因此这里只是简述这一部分基础。 堆内存对象可达性分析 在程序运行过程中,GC算法会在某一个时间点被触发,从而开启对整个堆区内存的分析,分析的结果是留下还在被业务线程使用的对象(可达对象),清理掉不再使用的对象(不可达对象)。对于这一过程,有很多的实现算法,比如引用计数法(Reference Counting)、GC根遍历法(GC Root Traversing)。GC根遍历法是多数GC算法使用的方式,所谓的GC根主要是全局的类对象指针,例如方法区上类的静态对象指针,业务线程栈帧上的对象指针。以这些对象指针作为起始点,开始遍历找到所有的可达对象并进行标记,那么剩下的未被标记的对象就是不可达对象,可以被清理掉的对象。 对象分配方式 到现在为止,Java在堆区分配对象的方式分为两种:一种是分代式对象分配,一种是页式对象分配。 对于分代式模型,主要的理论依据是不同对象的存活生命周期不同,80%的对象生命周期较短,20%的对象才是生命周期较长。分代式模型将堆内存划为两个不同的区域,Young区(生命周期较短的对象存储的区域)和Old区(生命周期较长的对象存储的区域)。对于new一个对象的请求,先将对象放到Young区,Young区的对象如果存活时间达到一定的阈值,那么将被迁移到Old区。对于Young区和Old区的对象,可以使用不同的回收算法进行不可达对象的分析和处理。 对于页式模型,将堆内存划分为大小相同的页,对于new一个对象的请求,将对象分配到一些指定的页上,GC过程中会将这些指定的页上的存活对象迁移到其他的页上,以便可以快速的回收这些页。 对象回收方式 对象回收过程需要面对的一个问题是回收后内存的整洁程度问题。如果只是清理不可达对象所对应的内存区域,那么经过多次的GC迭代,整个内存区域会变得非常零散,你不可能有一个平整的可用内存空间,进而导致有足够的内存却无法创建一个新的对象。对于分代式对象分配模型,对象回收过程在Young区使用复制模型(copy mode)将所有的可达对象集中复制到内存区域的一端。在Old区,类似Young区的复制模型,但是由于Old区较大,且内存区域不像Young区分分两段使用,复制的过程更加复杂。对于页式对象分配模型,会将指定页上的可达对象复制到其他页上,进而可以直接回收这些完全空白的页。 Z GC工作模型 模型概述 对于Z GC,一次完整的GC周期由多个阶段(Cycle)组成: Z GC相较于之前的GC算法,其STW可以稳定压缩的核心理念在于引入了染色指针和LB过程。染色指针是实施LB过程的先决条件,染色指针的核心在于其携带了指针所指向的对象的创建时机。LB基于这样的对象创建时机信息,判断是否需要对对象做特殊处理。基于这样的方式,可以实现两个目标(1)STW过程的时间长短只与GC Root中对象的数量有关,而与整个堆上对象的数量无关(2)业务线程不需要等待GC线程将所有的对象迁移到新的地址后才能运行,而是在运行过程中使用LB过程判断并发现悬空指针,而且可以基于染色指针只对少量的指针进行悬空指针判断。 染色指针 在64位操作系统上,Java应用的指针(引用)由8 byte/64 bit构成。对于Z GC,64 bit的指针由三部分组成,其中低42位为应用的实际地址空间,中间4位为指针颜色标识位(当前支持4种颜色,其中R/M1/M0为实际使用的颜色),高18位未使用。 三种颜色中M0/M1两种在连续的GC周期的STW-1阶段循环使用,如果Cycle N的SWT-1阶段使用颜色M0,那么Cycle N+1的STW-1阶段使用颜色M1,Cycle N+2的STW-1阶段使用颜色M0,Cycle N+3的STW-1阶段使用颜色M1,如果循环往复。所有的GC周期的STW-3阶段均使用颜色R。 染色指针的使用可以分为两个维度: 以Cycle N的SWT-1阶段为例,假设该阶段的全局颜色定义为M0。那么在其后的Marking/Remapping阶段,业务线程在加载指针时可能遇到三种颜色的指针。如果指针颜色为M0,那么可以判断该指针要么为STW-1阶段后新创建的对象的指针,要么为已经经历过指针颜色修复的指针(即该指针所指向的对象为上一GC周期创建的对象,LB过程在之前对该指针的访问过程中已经修复其颜色为当前有效颜色M0)。如果指针颜色为M1,那么该指针所指向的对象是上一GC周期的STW-1阶段到STW-3阶段之间创建的对象。如果指针颜色为R,那么该指针所指向的对象是上一GC周期的STW-3阶段到本周期的STW-1阶段之间创建的对象。Marking/Remapping阶段LB过程会对上一周期产生的颜色为M1/R的指针进行悬空指针修复(由于上一GC周的Relocation阶段会产生悬空指针)。 以Cycle N的STW-3阶段为例,该阶段的全局颜色定义为R。那么在其后的Relocation阶段,业务线程在加载指针时可能遇到两种颜色的指针。如果指针颜色为R,那么可以判断指针要么为STW-3阶段后新创建的对象的指针,要么为已经经历过指针颜色修复的指针。如果指针颜色为M0,那么该指针所指向的对象为本GC周期的STW-1阶段到STW-3阶段之间创建的对象或者是本GC周期的Marking/Remapping阶段所处理过的对象。Relocation阶段LB过程会对本周期产生的对象迁移任务进行辅助迁移和悬空指针修复。 STW-1 进入STW-1阶段表明Z GC预期应用的堆内存即将不足,并开启新一轮的堆内存对象回收以保证本轮GC结束时业务线程有足够的堆内存可以使用。应当知道的前提是(1)堆内存中对象指针包含两种可能的颜色,R+M0(M0对应于上一次GC Cycle的STW-1阶段定义的全局有效颜色)(2)堆内存中对象指针中可能存在悬空指针(悬空指针的定义见词汇表,概括讲是指上一个GC Cycle的Relocation阶段将部分对象的内存位置迁移了,指向这些对象的指针并没有随之修改导致这些指针变为悬空指针)。 Mark barrier过程被GC线程所使用,GC线程使用该过程判断GC Root指针是否为悬空指针,对于悬空指针使用remap操作将其修复,其中slot参数为含有GC Root指针的内存地址,addr为对应的GC Root指针值。Mark barrier判断如果addr为以上一个GC周期中指向Evacuation…