JVM垃圾收集算法与收集器(二)G1收集器、ZGC收集器详解

G1收集器

G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。一般G1收集器是用在8G以上内存的服务器上的,jdk9将它设为默认收集器。
image
image

G1内存中年轻代老年代结构和之前不同,以前年轻代老年代是有物理隔离的。G1现在年轻代和老年代只是一个逻辑概念,每一块区域都可能是年轻代,也可能之后会变成老年代。
它会将Java的堆分为多个大小相当的独立区域(称为Region),默认的情况下,是堆大小除以2048,即4G的堆的话,一块Region是2M大小。
默认情况下年轻代占比为5%,在系统运行中,jvm也会不断调整,增加年轻代的区域,但是默认不超过60%。年轻代中eden区和s区的大小比例依旧是8:1:1。
G1收集器对于对象什么时候转到老年区是和原来的原则一样的,唯一不同的是对大对象的处理,原先超过设定阈值大小的对象,会直接进入老年代。现在是有个专门的大对象保存区Humongous区,如果一个对象大小超过每个Region的50%,那么会直接进入Humongous中,Humorous卡页跨多个Region存放。
Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销。
Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。

G1的gc过程如下:

image

1.初始标记
2.并发标记
3.最终标记
4.筛选回收
1,2,3与CMS很像就不细说了,4的时候会开始垃圾回收,CMS的垃圾回收是与用户线程同步的,但是G1由于内部实现太复杂,所以没有实现并发回收。但是G1可以设定一个参数(可以用JVM参数 -XX:MaxGCPauseMillis指定),这个参数可以设定gc的停顿时间,它会根据这个停顿时间,来判断垃圾回收到什么程度。即G1不一定会把垃圾全部回收完,当它回收到一定程度,停顿时间到了,他就会停止回收,故它可能只会回收80%的垃圾,就继续让客户线程运行了。
回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个region中,这种不会像CMS那样
回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。

G1的垃圾收集分类

YoungGC
与其他的收集器的Minor gc不同,它不是在Eden区放满了触发,G1会计算下现在Eden区回收垃圾大概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么G1会选择继续增加年轻代的region,继续存放新对象,直到下一次Eden区放满,G1计算到回收时间接近参数 -XX:MaxGCPauseMills 设定的值,才会触发Young GC
MixedGC
类似之前其他的收集器的full gc,mixed gc会收集老年代,但是它不是FullGC,它会在老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的年轻代和部分老年代(根据期望的GC停顿时间确定老年代垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空间去承载被拷贝的对象就会触发一次Full GC
Full GC
这个时候会停止用户的线程,采用单线程的方式去gc回收,这个过程十分耗时。

G1收集器参数

-XX:+UseG1GC:使用G1收集器
-XX:ParallelGCThreads:指定GC工作的线程数量
-XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个分区
-XX:MaxGCPauseMillis:目标暂停时间(默认200ms)
-XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)
-XX:G1MaxNewSizePercent:新生代内存最大空间
-XX:InitiatingHeapOccupancyPercent:老年代占用空间达到整堆内存阈值(默认45%),则执行MixedGC,比如我们之前说的堆默认有2048个region,如果有接近1000个region都是老年代的region,则可能就要触发MixedGC了
-XX:G1MixedGCLiveThresholdPercent(默认85%) region中的存活对象低于这个值时才会回收该region,如果超过这个值,存活对象过多,回收的的意义不大。
-XX:G1MixedGCCountTarget:在一次回收过程中指定做几次筛选回收(默认8次),在筛选回收阶段,G1可以回收一会,然后暂停回收,放用户线程运行一会,一会再开始回收,这样可以让系统不至于单次停顿时间过长,这个参数可以设置暂停的次数,如果系统垃圾不多可以设小一点。
-XX:G1HeapWastePercent(默认5%): gc过程中空出来的region是否充足阈值,主要是用来判断什么时候停止mixed gc的,在mixed gc的时候,回收过程会一直清理空间,一旦清理出来的空闲Region数量达到了堆内存的5%,此时就会立即停止mixed gc。

ZGC收集器

ZGC收集器是jdk11中加入的收集器,目前还不完善,它支持TB级内存的垃圾收集,且最大GC停顿时间不超过10ms,它的堆内存结构是不分代的。而是分为了小内存(2M),中内存(32M)和大内存(2N M)。
image

染色指针

image
ZGC和CMS、G1不同的地方在于,CMS、G1是将三色标记做在对象上的,ZGC是做在指针上的。目前Linux下64位指针的高18位不能用来寻址,剩余的46位指针其结构为:
1位:Finalizable标识,此位与并发引用处理有关,它表示这个对象只能通过finalizer才能访问;
1位:Remapped标识,设置此位的值后,对象未指向relocation set中(relocation set表示需要GC的
Region集合);
1位:Marked1标识;
1位:Marked0标识,和上面的Marked1都是标记对象用于辅助GC(三色标记);
42位:对象的地址(所以它最大支持2^42=4T内存

为什么有2个mark标记?每一个GC周期开始时,会交换使用的标记位,使上次GC周期中修正的已标记状态失效,所有引用都变成未标记。
GC周期1:使用mark0, 则周期结束所有引用mark标记都会成为01。
GC周期2:使用mark1, 则期待的mark标记10,所有引用都能被重新标记。

染色指针的优势

1.染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指令向该Region的引用都被修正后才能清理。
2.染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是在写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作。
3.染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。

ZGC的执行过程

image
1.并发标记(Concurrent Mark): 与G1一样,并发标记是遍历对象图做可达性分析的阶段,前后也要经过类似于G1的初始标记、最终标记的短暂停顿,而且这些停顿阶段所做的事情在目标上也是相类似的。不同的是ZGC标记的是指针而不是对象。
2.并发预备重分配( Concurrent Prepare for Relocate): 这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。
3.并发重分配(Concurrent Relocate): 重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。
4.并发重映射(Concurrent Remap): 重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,ZGC的并发映射并不是以一个必须要“迫切”去完成的任务。ZGC很巧妙地把并发重映射阶段要做的工作,合并到下一次垃圾收集循环中的并发标记阶段里去完成,反正他们都是要遍历所有对象的,这样合并节省了一次遍历的开销。

ZGC的优劣势

缺点:浮动垃圾
当ZGC准备要对一个很大的堆做一次完整的并发收集,驾驶其全过程要持续十分钟以上,由于应用的对象分配速率很高,将创造大量的新对象,这些新对象很难进入当次收集的标记范围,通常就只能全部作为存活对象来看待(尽管其中绝大部分对象都是朝生夕灭),这就产生了大量的浮动垃圾。
目前唯一的办法就是尽可能地去增加堆容量大小,获取更多喘息的时间。但若要从根本上解决,还是需要引入分代收集,让新生对象都在一个专门的区域中创建,然后针对这个区域进行更频繁、更快的收集。
优点:高吞吐量、低延迟。
ZGC是支持“NUMA-Aware”的内存分配。MUMA(Non-Uniform Memory Access,非统一内存访问架构)是一种多处理器或多核处理器计算机所设计的内存架构。
现在多CPU插槽的服务器都是Numa架构,Numa的架构是所有CPU去争抢一个内存,那么肯定会出现并发的问题和锁的问题,因此速度会慢。
ZGC默认支持NUMA架构,在创建对象时,根据当前线程在哪个CPU执行,优先在靠近这个CPU的内存进行分配,这样可以显著的提高性能。

记忆集与卡表

记忆集的作用是,gc root的时候可能会遇到跨代引用的问题,即老年代引用了年轻代。gc root指的是内存中的静态变量,和临时变量,这样的话是没办法得到老年代中的对象引用情况的。如果没有别的办法,只能一个个去扫老年代,这样效率十分低下。因此加入了记忆集这个数据结构,它的作用是记录从非收集区到收集区的指针集合,避免了将老年代也加入gc root的扫描范围。
hotspot使用卡表的方式实现了记忆集。
卡表是一个字节数组实现的对象,在老年代中存在“卡页”,记录了引用的内存地址。年轻代中存着“卡表”是一个字节数组,类似“0101”的数据,其中1代表老年代中的卡页关联着年轻代,即它是Dirty的。这个时候就需要去找相应索引的卡页。
hotspot中使用了写屏障来维护卡表状态。
G1收集器中在每个Region都有一个卡表,CMS收集器中则是年轻代有卡表,老年代有卡页

posted @ 2022-03-21 23:14  君子酱  阅读(937)  评论(0编辑  收藏  举报