垃圾收集器-ZGC
布局结构
同G1一样,ZGC也采用了基于Region的布局方式,但是与G1的固定大小不同,ZGC将Region分为三种类型:
- 小型Region:容量固定为2MB,用于存放小于256KB的小对象。
- 中型Region:容量固定为32MB,用于放置大于256KB但小于4MB的对象。
- 大型Region:容量不固定,可以动态变化,但是必须是2MB的整数倍,用于放置4MB以上的对象。每个大型Region只会存放一个对象,这也说明可能存在一个大Region比中型Region还小的情况。 在大型Region中的对象是不会参与重分配的
染色指针
ZGC有个标志性的设计就是它所采用的染色指针技术。以前我们想从对象上存储一些额外信息:比如分代年龄,三色标记等,通常会存储在对象头中。 在垃圾收集场景中,我们通常会使用三色标记来标记一个对象是否存活,不同的垃圾收集器有不同的实现:比如Serial是记录在对象头,G1是采用额外数据结构来记录。 而在ZGC上,则是直接把标记信息记录在指针上,这也就是染色指针技术。
在64位系统中,寻址地址理论上可以有64位,这也意味着64位系统允许的内存大小为16EB,但从硬件限制条件来看,只能支持到48位,也就是256TB的内存大小。 在Linux上,分别支持47位进程虚拟地址和46位物理地址空间。因此,在一般服务器上,所能使用的地址指针为46位。
鉴于此,ZGC的染色指针使用了这46位指针宽度,将其高4位提取出来存储4个标志信息:
|高18位,操作系统限制|Finalizable|Remapped|Marker1|Marker0|42位地址信息|
这4个标志位分别是:
- Finalizable:标记当前对象是否是虚引用
- Remapped:是否进入了重分配
- Marker0|1:两个标志位,即三色标记
因此实际在ZGC环境中,所能支持的内存空间大小是2^42b,也就是4TB,也足够满足如今大部分大型服务器的需求了。而且其带来的收益是非常客观的,在JEP333 中,设计者是这样描述染色指针的优势:
- 染色指针可以保证某个Region区域存活的对象被移动走后,这个Region可以立刻就被释放,不必等待整个堆中所有指向该Region的引用都被修正后再清理。
- 染色指针可以大幅减少内存屏障的使用,设置内存屏障本意是记录对象引用的变更,有了染色指针后,可以直接维护在指针中。
- 染色指针可以作为一种可拓展的结构来记录更多信息。
多重地址映射
遗憾的是,x86-64平台上只会把指针视为一个内存地址来对待,因此一旦对指针的某一位进行了改变,则程序在执行过程中会指向另外一段未知的内存,这是亟需解决的问题。
解决这个问题用到了虚拟内存映射技术,我们先来了解下这个经典设计:
在远古时代,所有的进程都是共用一块物理内存空间的,这样会导致进程与进程间的内存无法相互隔离,当一个进程污染了别的进程内存后,就只能对整个系统进行复位才能恢复。 为此,引入了虚拟内存映射技术,处理器会使用分页管理机制,把线性地址空间和物理地址空间划分为大小相同的内存块,也就是“页”。然后在线性地址空间和物理地址空间之间建立一个映射表。 这样就可以完成线性地址到物理地址的转换。
ZGC使用多重地址映射(Multi-Mappings)将多个虚拟地址空间映射到同一个物理地址上,也就说对于每个染色指针,以标志位作为分段符,后42位地址确定一个真实的物理空间。当然这也会导致在虚拟机中看到的地址空间要比实际的物理空间要大。
工作流程
ZGC的运作过程可以大致划分为四个阶段:
- 并发标记,与G1类似,也要对
GC Roots
之间可达对象进行标记并做可达性分析的阶段,而ZGC的标记是对染色指针进行操作 - 并发预备重分配,这个阶段主要计算出需要清理哪些Region,将这些Region组成重分配集(Relocation Set)。有点类似于G1的回收集,但是不同的是ZGC会对所有的重分配集进行回收,而不是只寻找回收价值最高的集合。 同时,ZGC每次都会扫描所有的Region,用于确定重分配集,重分配集里存活的对象,会重新复制到其他空闲的Region中,原有的Region会被清理。
- 并发重分配,这个过程是把存活的对象复制到新的Region上,同时维护一个转发表,用于记录复制后的新地址。同时染色指针上Remapped标记位会标记当前指针是否被转发表映射过。
- 并发重映射,这个过程是修正整个堆中指向重分配集中旧对象的所有引用,但是这个过程并不是一个很迫切的任务,因为转发表的存在,所以即使不更新,也可以定位到复制后的对象位置,所可以留到下次GC触发时再次同步更新
读屏障
前面提及过转发表的左右,在并发重分配阶段,原有存活的对象会复制到新的区域,此时转发表会记录下新的地址,与原先旧的地址形成一个映射,这样后续访问旧地址的引用时,可以通过染色指针判断是否移动过。移动过则根据转发表进行转发,同时修正引用,这个过程被称为“自愈”。
前面CMS和G1都有用过写屏障用于记录位置的变更。这里,ZGC的“自愈”过程则采用了读屏障来实现,在第一次读引用时,会判断一次是否需要更新引用,后续就无需判断了。