G1垃圾收集器(-XX:+UseG1GC)详解
G1(Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特性。
G1把内存区域划分为小格子(Region),最多可以有2048个Region,一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,每个小格子也是有分代概念的。默认年轻代对堆内存的占比时5%,如果堆大小为4096M,那么年轻代占据200M左右的内存,大概时100个Region。一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是说Region的区域功能可能会动态变化。
G1垃圾收集器对于对象什么时候会转移到老年代根之前讲过的原则一样,唯一不同的是对大对象的处理,G1有专门的大对象存储区Humongous,当放到Eden区时超过50%,则放到Humongous区。
G1收集器一次GC的运作大致分为以下几个步骤:
- 初始标记:STW,并记录下gc roots直接能引用的对象,速度很快;
- 并发标记:并发标记阶段是从GC Root的直接关联对象开始遍历整个对象的过程,这个过程耗时较长,但不需要停顿用户线程,可以与垃圾收集器线程一起并发运行。因为用户线程继续运行,可能会导致已经标记的对象状态发生改变。
- 最终标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记时间短,主要用到三色标记里的增量更新算法啊,做重新标记。
- 筛选回收:筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,比如说老年代此时有1000个Region都满了,但是因为预期停顿时间,本次垃圾回收只能停顿200毫秒,那么通过之前回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region。筛选回收不一定回收所有的。底层用了复制算法,即区域如果有20%的非垃圾对象,会将其复制到相邻区域,剩下的清理掉,复制很少碎片。
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region,比如一个Region花200ms能回收10M垃圾,另外一个Region花50ms能回收20M垃圾,在回收时间有限情况下,G1当然会优先选择后面这个Region回收。
最终标记G1用的原始快照,CMS用的增量更新,最大停顿时间可设,使用的类似标记整理算法,底层用复制算法。如果最多只能回收4个Region,提前做计算,回收时间短的Region。存活的对象越多,回收垃圾的效益比越低。
G1垃圾收集分类:
Yong GC:触发Yong GC时,会计算判断这次GC回收的时间是不是接近200ms,如果接近做Yong GC,如果没有则丢到新区域。
Mixed GC:老年代的堆根据-XX:InitiatingHeapOccupancyPercent设定的值触发,回收所有的年轻代和部分Old,(根据最大停顿时间回收效益比,可能只回收一部分),大对象区域也会回收。主要使用复制算法,拷贝过程中如果发现没有足够空的region能够承载拷贝对象就会触发一次Full GC。
Full GC:底层只用单线程回收(老年代没有足够的Region供拷贝时触发)。
每个Eden区都有一个卡表,记录那块区域的引用,每个小的放个都有一个卡表,记录别的老年代区域对当前区域年轻代的引用。
G1垃圾收集器优化建议
假设参数-XX:MaxGCPauseMills设置的值很大,导致系统运行很久,年轻代可能都占用了堆内存的60%了,此时才出发年轻代gc。
那么存活下来的对象可能就会 很多,此时就会导致Survivor区域放不下那么多对象,就会进入老年代。
这里核心还是在与调节-XX:MaxGCPauseMills这个参数的值,在保证他的年轻代GC别太频繁的同时,还得考虑每次GC过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixed gc.
ZGC收集器(-XX:+UseZGC)
ZGC目标
- 支持TB量级的堆:最大支持 16TB 的大堆,最小支持 8MB 的小堆
- 最大GC停顿时间不超10ms
- 奠定未来GC特性的基础
- 最糟糕的情况下吞吐量会降低15%:跟 G1 相比,对应用程序吞吐量的影响小于 15 %
ZGC内存布局
ZGC是一款基于Region内存布局的,使用了读屏障、颜色指针等技术来实现可并发的标记-整理算法,以低延迟为首要目标的垃圾收集器。
ZGC的Region有大、中、小三个容量:
- Small Region:容量固定为2M,用于存放小于256KB的小对象。
- Medium Region:容量固定为32M,用于存放大于等于256K但小于4M的对象。
- Large Region:容量不固定,可动态变化,但必须是2MB得倍数,用于存放大于等于4M的对象。大型Region在ZGC的实现中是不会被重分配的,因为复制一个大对象的代价非常昂贵。
NUMA-aware
NUMA就是Non Uniform Memory Access Architecture,UMA模式内存只有一块,存在争抢问题,有争抢就会有锁,影响效率,而且CPU核数越多竞争越激烈。NUMA每块CPU对应一块内存,且这块内存在主板上离这个CPU是最近的,每个CPU优先访问这块内存,那效率自然就提高了。
读屏障
读屏障类似于 Spring AOP 的前置增强,是 JVM 向应用代码中插入一小段代码,当应用线程从堆中读取对象的引用时,会先执行这段代码。注意:只有从堆内存中读取对象的引用时,才会执行这个代码。下面代码只有第一行需要加入读屏障。
Object o = obj.FieldA
Object p = o //不是从堆中读取引用
o.dosomething() //不是从堆中读取引用
int i = obj.FieldB //不是引用类型
读屏障在解释执行时通过 load 相关的字节码指令加载数据。作用是在对象标记和转移过程中,判断对象的引用地址是否满足条件,并作出相应动作。如下图:
颜色指针
每个对象都有64位指针,这64位被分为:
- 16位:预留给以后使用
- 1位:Finalizable标识,此位与并发引用处理有关,它标识这个对象这能通过finalizer才能访问
- 1位:Remapped标识,设置后,对象未指向relocation set中(relocation set表示需要GC的Region集合 )
- 1位:Marked1标识
- 1位:Marked0标识,和Marked1都是标记对象用于辅助GC
- 44位:对象的地址(所以它可以支持2^44=16T内存)
GC过程
- 初始标记:从 GC Roots 出发,找出 GC Roots 直接引用的对象,放入活跃对象集合,这个过程需要 STW,不过 STW 的时间跟 GC Roots 数量成正比,耗时比较短
- 并发标记:与G1一样,并发标记是遍历对象图做可达性分析的阶段,它的初始标记(Mark Start)和最终标记(Mark End)也会出现短暂的停顿,与G1不同的是,ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0、Marked 1标志位。
- 再标记:并发标记阶段 GC 线程和 Java 应用线程并发执行,标记过程中可能会有引用关系发生变化而导致的漏标记问题。再标记阶段重新标记并发标记阶段发生变化的对象,还会对非强引用(软应用,虚引用等)进行并行标记。
这个阶段需要 STW,但是需要标记的对象少,耗时很短。 - 初始转移:转移就是把活跃对象复制到新的内存,之前的内存空间可以被回收。
初始转移需要扫描 GC Roots 直接引用的对象并进行转移,这个过程需要 STW,STW 时间跟 GC Roots 成正比。 - 并发转移:重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障(读屏障)所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。
- 并发重映射(Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在“自愈”功能,所以这个重映射操作并不是很迫切。ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后, 原来记录新旧对象关系的转发表就可以释放掉了。
参考: 12 张图带你彻底理解 ZGC