Garbage First (简称G1)收集器开创了收集器面向局部收集的设计思路和基于 Region 的内存布局形式。G1 从整体来看是基于“标记-"整理”算法实现的收集器,但从局部(两个 Region 之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。G1 是一款主要面向服务端应用的垃圾收集器。
G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的 Java 堆划分为多个大小相等的独立区域(Region),每一个 Region 都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间或者老年代空间。收集器能够对扮演不同角色的 Region 采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。Region 中还有一类特殊的 Humongous 区域,专门用来存储大对象。G1认为只要大小超过了一个 Region 容量一半的对象即可判定为大对象。每个 Region 的大小可以通过参数-XX:G1HeapRegionSize 设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个 Region 容量的超级大对象,将会被存放在N个连续的 Humongous Region之中,G1的大多数行为都把 Humongous Region 作为老年代的一部分来进行看待。
虽然 G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域 (不需要连续)的动态集合。G1 收集器之所以能建立可预测的停顿时间模型是因为它将 Region 作为单次回收的最小单元,即每次收集到的内存空间都是 Region 大小的整数倍,这样可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。更具体的处理思路是让 G1 收集器去跟踪各个 Region 里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间 (使用参数-XX:MaxGCPauseMillis 指定,默认值是 200 毫秒)优先处理回收价值收益最大的那此 Rerion,这也就是“Garbage First”名字的由来。这利使用 Region 划分内存空间,以及具有优先级的区域回收方式,保证了 G1 收集器在有限的时间内获取尽可能高的收集效率。
在 G1收集器出现之前的所有其他收集器,包括 CMS 在内,垃圾收集的目标范围要么是整个新生代 (MinorGC),要么就是整个老年代 (Major GC),再要么就是整个 Java 堆 (Full GC)。而GI跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集 (Colletion Set,一般简称 CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是 G1 收集器的 Mixed GC 模式。
G1收集器的运作过程大致可划分为以下四个步骤:
初始标记(Initial Marking): 仅仅只是标记一下 GC Roots 能直接关联到的对象并且修改 TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的 Region 中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC 的时候同步完成的,所以 G1 收集器在这个阶段实际并没有额外的停顿。(TAMS (Top at Mark Start):G1为每一个Region 设计了两个名为TAMS 的指针,把Region 中的部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上)
并发标记(Concurrent Marking):从GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理原始快照(SATB)记录下的在并发时有引用变动的对象。
最终标记 (Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结变动的对象。束后仍遗留下来的最后那少量的原始快照(SATB)记录。
筛选回收 (Live Data Counting and Evacuation):负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
从上述阶段的描述可以看出,G1 收集器除了并发标记外,其余阶段也是要完全暂停用户线程的,换言之,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才能担当起“全功能收集器”的重任与期望。
Shenandoah 也是使用基于 Region堆内存布局,同样有着用于存放大对象的 Humongous Region,默认的回收策略也同样是优先处理回收价值最大的 Region,支持并发的整理算法。其次,默认不使用分代收集的。最后改用名为“连接矩阵”(Connection Matrix)的全局数据结构来记录跨 Region 的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题的发生概率。
连接矩阵可以简单理解为一张二维表格,如果 Region N 有对象指向 Region M,就在表格的 N行M列中打上一个标记,如图3-15 所示,如果 Region 5 中的对象 Baz引用了 Region 3 的 Foo,Foo又引用了 Region 1的 Bar,那连接矩阵中的 5行3 列、3行1列就应该被打上标记。在回收时通过这张表格就可以得出哪些 Region 之间产生了跨 Region 的引用。
Shenandoah 收集器的工作过程大致可以划分为以下九个阶段:
初始标记(Initial Marking):首先标记与 GC Roots 直接关联的对象,这个阶段仍是“Stop The World”的,但停顿时间与堆大小无关,只与 GC Roots 的数量相关。
并发标记(Concurrent Marking):遍历对象图,标记出全部可达的对象这个阶段是与用户线程一起并发的,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。
最终标记 (Final Marking):处理剩余的原始快照( SATB) 扫描,并在这个阶段统计出回收价值最高的 Region,将这些 Region 构成一组回收集(Collection Set)。最终标记阶段也会有一小段短暂的停顿。
并发清理(Concurrent Cleanup): 这个阶段用于清理那些整个区域内连一个存活对象都没有找到的 Region(这类 Region 被称为Immediate Garbage Region)
并发回收(Concurrent Evacuation):把回收集里面的存活对象先复制一份到其他未被使用的 Region 之中。复制对象这件事情如果将用户线程冻结起来再做那是相当简单的,但如果两者必须要同时并发进行的话,就变得复杂起来了。其困难点是在移动对象的同时,用户线程仍然可能不停对被移动的对象进行读写访问,移动对象是一次性的行为,但移动之后整个内存中所有指向该对象的引用都还是旧对象的地址,这是很难一瞬间全部改变过来的。对于并发回收阶段遇到的这些困难,Shenandoah 将会通过读屏障和被称为“Brooks Pointers”的转发指针来解决。并发回收阶段运行的时间长短取决于回收集的大小。
初始引用更新 (Initial Update Reference): 并发回收阶段复制对象结束后,还需要把堆中所有指向旧对象的引用修正到复制后的新地址,这个操作称为引用更新。引用更新的初始化阶段实际上并未做什么具体的处理,设立这个阶段只是为了建立一个线程集合点,确保所有并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务而已。初始引用更新时间很短,会产生一个非常短暂的停顿。
并发引用更新(Concurrent Update Reference): 真正开始进行引用更新操作,这个段是与用户线程一起并发的,时间长短取决于内存中涉及的引用数量的多少。并发引用更新与并发标记不同,它不再需要沿着对象图来搜索,只需要按照内存物理址的顺序,线性地搜索出引用类型,把旧值改为新值即可。
最终引用更新 ( Final Update Reference): 解决了堆中的引用更新后,还要修正存于GC Roots 中的引用。这个阶段是 Shenandoah 的最后一次停顿,停顿时间只与GC Roots 的数量相关。
并发清理 (Concurrent Cleanup):经过并发回收和引用更新之后,整个回收集中所的 Region 已再无存活对象,这些 Region 都变成 Immediate Garbage Regions 了,最后再调用一次并发清理过程来回收这些 Region 的内存空间,供以后新对象分配使用。
图 3-16中黄色的区域代表的是被选回收集的 Region,绿色部分就代表还存活的对象,蓝色就是用户线程可以用来分配对象的内存 Region 了。图3-16中不仅展示了 Shenandoah 三个并发阶段的工作过程,还能形象地表示出并发标记阶段如找出回收对象确定回收集,并发回收阶段如何移动回收集中的存活对象,并发引用更新阶段如何将指向回收集中存活对象的所有引用全部修正,此后回收集便不存在任何引用可达的存活对象了
ZGC 收集器是一款基于 Region 内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。
首先从ZGC 的内存布局说起。与 Shenandoah 和 G1一样,ZGC 也采用基于 Region 的堆内存布局,但与它们不同的是,ZGC 的 Region(在一些官方资料中将它称为 Page 或者ZPage)具有动态性一动态创建和销毁,以及动态的区城容量大小。在x64 硬件平台下,ZGC 的 Region 可以具有的大、中、小三类容量:
小型 Region (Small Region):容量固定为 2MB,用于放置小于 256KB 的小对象。
中型 Region (Medium Region):容量固定为32MB,用于放置大于等于 256KB 但小于 4MB 的对象。
大型 Region (Large Region):容量不周定,可以动态变化,但必须为 2MB 的整数倍用于放置4MB 或以上的大对象。每个大型 Region 中只会存放一个大对象,这也预示着虽然名字叫作“大型 Region”,但它的实际容量完全有可能小于中型 Region,最小容量可低至 4MB。大型 Region 在 ZGC 的实现中是不会被重分配(重分配是ZGC的一种处理动作,用于复制对象的收集器阶段,稍后会介绍到)的,因为复制一个大对象的代价非常高昂。
ZGC 收集器有一个标志性的设计是它采用的染色指针技术 (Colored Pointer,其他类似的技术中可能将它称为 Tag Pointer 或者 Version Pointer)。染色指针是最直接的、最纯粹的,它直接把标记信息记在引用对象的指针上,这时,与其说可达性分析是遍历对象图来标记象,还不如说是遍历“引用图”来标记“引用”了。
Linux 下64 位指针的高 18 位不能用来寻址,但剩余的 46 位指针所能支持的 64TB内存在今天仍然能够充分满足大型服务器的需要。鉴于此,ZGC 的染色指针技术继续盯上了这剩下的 46 位指针宽度,将其高 4 位提取出来存储四个标志信息。通过这些标志位,虚拟机可以直接从指针中看到其引用对象的三色标记状态、是否进入了重分配集 (即被移动过)、是否只能通过 finalize() 方法才能被访问到。当然,由于这些标志位进一步压缩了原本就只有 46 位的地址空间,也直接导致 ZGC 能够管理的内存不可以超过4TB(2的42 次幂)。
染色指针可以使得一旦某个 Region 的存活对象被移走之后,这个 Region 立即就能够被释放和重用掉,而不必等待整个堆中所有指向该 Region 的引用都被修正后才能清理。这点相比起 Shenandoah 是一个颇大的优势,使得理论上只要还有一个空闲Region,ZGC 就能完成收集,而 Shenandoah 需要等到引用更新阶段结束以后才能释放回收集中的 Region,这意味着堆中几乎所有对象都存活的极端情况,需要 1:1复制对象到新 Region 的话,就必须要有一半的空闲 Region 来完成收集。
染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作。实际上,到目前为止 ZGC 都并未使用任何写屏障,只使用了读屏障(一部分是染色指针的功劳,一部分是 ZGC 现在还不支持分代收集,天然就没有跨代引用的问题)。能够省去一部分的内存屏障,显然对程序运行效率是大有神益的,所以 ZGC 对吞吐量的影响也相对较低。
染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。现在 Linux 下的64 位指针还有前 18 位并未使用,它们虽然不能用来寻址,却可以通过其他手段用于信息记录。如果开发了这18 位,既可以腾出已用的4个标志位,将 ZGC可支持的最大堆内存从4TB 拓展到64TB,也可以利用其余位置再存储更多的标志,譬如存储一些追踪信息来让垃圾收集器在移动对象时能将低频次使用的对象移动到不常访问的内存区域。
Linux/x86-64 平台上的 ZGC使用了内存多重映射(Multi-Mapping),将多个不同的虚拟内存地址映射到同一个物理内存地址上,这是一种多对一映射,意味着ZGC在虚拟内存中看到的地址空间要比实际的堆内存容量来得更大。把染色指针中的标志位看作是地址的分段符,那只要将这些不同的地址段都映射到同一个物理内存空间,经过多重映射转换后,就可以使用染色指针正常进行寻址了。
ZGC 的运作过程大致可划分为以下四个大的阶段:
并发标记( Concurrent Mark):与G1、Shenandoah 一样,并发标记是遍历对象图做可达性分析的阶段,前后也要经过类似于 G1、Shenandoah 的初始标记、最终标记(尽管 ZGC 中的名字不叫这些)的短暂停顿,而且这些停顿阶段所做的事情在目标上也是相类似的。与 G1、Shenandoah 不同的是,ZGC 的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的 Marked 0、Marked 1 标志位。
并发预备重分配( Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set)。重分配集与 G1 收集器的回收集(Collection Set) 还是有区别的,ZGC划分 Region 的目的并非为了像 G1那样做收益优先的增量回收。相反,ZGC每次回收都会扫描所有的 Region,用范围更大的扫描成本换取省去 G1 中记忆集的维护成本。因此,ZGC 的重分配集只是决定了里面的存活对象会被重新复制到其他的 Region 中,里面的 Region 会被释放,而并不能说回收行为就只是针对这个集合里面的 Region 进行,因为标记过程是针对全堆的。此外,在JDK 12的ZGC 中开始支持的类卸载以及弱引用的处理,也是在这个阶段中完成的。
并发重分配(Concurrent Relocate):重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的 Region 上,并为重分配集中的每个 Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。得益于染色指针的支持,ZGC 收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据 Region 上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。这样做的好处是只有第一次访问旧对象会陷人转发,也就是只慢一次,对比 Shenandoah 的 Brooks 转发指针,那是每次对象访向都必须付出的周究开销,简单地说就是每次都慢,因此,ZGC 对用户程序的行时负我要此出的研定嫌得更低一些。还有另外一个直接的好处是由于染色针的存在,一旦重分配集中某个Region的存活对象都复制完毕后,这个 Reion就可以立即释放用于新对象的分配(但是转发表还得留着不能释放掉),哪怕堆中还很多指向这个对象的未更新指针也没有关系,这些旧指针一旦被使用,它们都是以自愈的。
并发重映射 (Concurrent Remap): 重映射所做的就是修正整个堆中指向重分配集旧对象的所有引用,“这二点从目标角度看是与 Shenandoah 并发引用更新阶段一样的,但是ZGC 的并发重映射并不是一个必须要“追切”去完成的任务,因为前面过,即使是旧引用,它也是可以自愈的,最多只是第一次使用时多一次转发和修操作。重映射清理这些旧引用的主要目的是为了不变慢(还有清理结束后可以释放”转发表这样的附带收益)。所以说这并不是很“迫切”。因此,ZGC 很巧妙地把并重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后,原来记录新旧对象关系的转发表就可以释放掉了。
ZGC 中的几种种触发 GC场景:
基于固定时间间隔:默认为不使用,可以通过 ZCollectionInterval 参数配置。GC 日志中的关键字 “Timer”。
启动预热触发:最多三次,在堆内存空间达到 10%、20%、30% 时机触发、主要是通过 GC 的时间、为其他的 GC 触发准备。GC日志关键字 “Warmup”。
基于分配速率的自适应算法:基于正态分布统计,计算内存 99% 可能的最大分配速率,以及此速率下内存将要耗尽的时间点,在耗尽之前触发 GC (耗尽时间,一次 GC 最大持续时间-一次 GC 检测周期时间)。GC日志关键字 “Allocation Rate”。
主动触发:(默认开启,可以通过 ZProactictive 参数配置)距上一次 GC 堆内存增长 10%,超过 5 分钟时,对比上次 GC的间隔时间限(一次 GC 最大持续时间),超过则触发。GC 日志关键字 “Proactive”。
元数据分配触发:元数据区不足导致,GC 日志关键中是 “Metadata GC Threshold”
直接触发:代码中显示调用 System.gc() 触发,GC 日志关键字是 “System.gc()”。
阻塞内存分配请求触发:垃圾对象来不及挥手,占满整个堆空间,导致部分线程阻塞,GC 日志关键字是 “Allocation Stall”
借道友法力一用:
========================== stay hungry stay foolish =============================