最近王子因为个人原因有些忙碌,导致文章更新比较慢,希望大家理解,之后也会持续和小伙伴们一起共同分享技术干货。
上篇JVM的文章中我们对ParNew和CMS垃圾回收器已经有了一个比较透彻的认识,感兴趣的小伙伴可以去回看一下探索ParNew和CMS垃圾回收器。
今天我们继续探索垃圾回收器G1的原理,让我们开始吧!
G1的内存模型
G1是从jdk7开始出现的,在jdk9中被设为默认垃圾收集器,目标就是彻底替换掉CMS,那么为什么它可以替换掉CMS呢?
首先我们就来看看它的内存模型吧。
其实G1是可以同时回收年轻代和老年代的,他最大的特点就是把jvm堆内存拆分为了多个大小相等的Region,那么还存在年轻代和老年代吗?
答案是肯定的,不同的是新生代可能包含了某些Region,老年代也可能包含了某些Region,如下图:
到底有多少Region?每个Region有多大呢?
其实这个默认情况下是自动计算的,假如我们给定整个堆内存大小为4096M,然后使用“-XX:+UseG1GC”指定垃圾回收器为G1,此时会自动用堆内存大小除以2048,因为JVM最多可以有2048个Region,然后Region的大小必须是2的倍数。
堆内存为4096M,就会分配给每个Region 2M的内存空间。我们使用G1默认的计算方式就可以了。
当然也可以通过参数“-XX:G1HeapRegionSize”来指定Region的大小。
新生代和老年代的默认比例是多少呢?
我们知道使用ParNew和CMS垃圾回收器时,新生代和老年代的默认比例是1:2,而使用G1后,默认新生代对堆内存的初始占比是5%,这个可以通过“-XX:G1NewSizePercent”来设置初始占比,一般不需要设置。
细心的小伙伴会发现,这里说的占比是初始占比,因为系统运行的时候,JVM其实会不停的给新生代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent”来设置。
而一旦发生了垃圾回收,新生代的Region数量还会减少,所以其实新生代和老年代的占比不是一成不变的,而是动态改变的。
新生代还有eden和survivor吗?
答案是肯定的,新生代还是有eden和survivor的,只不过内存占用会随着Region的增多而增大。
G1的停顿时间控制
除了内存的变化,G1还有一个最大的变化,就是可以让我们设置一个垃圾回收的预期停顿时间,也就是说我们可以指定G1垃圾回收导致“Stop the World”的最长时间。
我们知道JVM一大痛点就是"Stop the World",尽量减少它的时间就可以做到JVM的优化。
引入G1后,我们可以自己去设定这个停顿的最长时间了,相当于直接控制了垃圾回收的性能。
G1要做到这一点就要去追踪每个Region的回收价值,那什么是回收价值呢?大家看下图:
比如两个Region中,其中一个有10M的垃圾对象,垃圾回收需要耗时1s,另一个有20M的垃圾对象,垃圾回收耗时200ms。
然后G1进行垃圾回收的时候,发现最近1小时垃圾回收已经导致了几百毫秒的系统停顿了,所以会选择回收价值高的Region进行回收,200ms的时间就能回收掉20M的垃圾对象,回收价值相对较高,所以会选择这个Region进行回收
G1控制停顿时间的思路,简单来讲就是,它会通过跟踪Region的回收价值,尽可能的保证系统停顿时间在你设定的停顿时间范围内。
G1的垃圾回收详解
上文我们了解到新生代还是有eden和survivor的,那么随着新生代占据堆内存大小的60%的时候,这个时候就会触发新生代的GC,G1也会使用之前我们说过的复制算法进行垃圾回收,进入一个“Stop the World”状态。
但是这个过程与之前的Minor GC其实是有差别的,首先回收的对象变成了带有垃圾对象的Region,然后回收的同时会根据设定的停顿时间进行价值回收,如上文所述。
什么时候进入老年代呢?
这个可以说和之前是一模一样的,简单介绍如下:
新生代躲过多次垃圾回收后会进入老年代;
GC后存活对象超过Survivor区的50%,那么会触发动态年龄判定规则,符合规则的进入老年代。
具体细节不在说明,可以参考王子之前的文章秒懂JVM的垃圾回收机制,有详细解释。
需要注意的是,G1的大对象不是存到老年代中的,而是提供了专门的Region来存放大对象。
在G1中,大对象的判断规则就是这个对象超过了一个Region大小的50%,比如Region是2M的,那如果你的对象超过了1M,就会被认定为大对象,做特殊处理。
而且如果这个大对象过大,可以横跨多个Region进行存储,如下图: