G1是一款面向服务器的垃圾收集器,主要针对配备多颗处理器以及大容量内存的机器。以极高概率满足GC停顿时间要求的同时,还具备高吞吐量的特征。
G1的内存布局与CMS等垃圾回收器都不相同。G1虽然仍保留了年轻代、老年代的概念,但它不在是一大块连续的内存。

G1将堆划分成了若干个大小相同的Region,JVM最多可以有2048个Region(默认也是)。如一个4GB的堆内存,将会被划分成2048个Region,每个Region大小为2MB。
默认情况下,年轻代堆内存占比为5%,如果堆大小为4096MB,那么年轻代占据200MB左右的内存,对应大概100个Region,我们可以通过“-XX:G1NewSizePercent”设置新生代初始占比,在系统运行中,JVM会不停的给年轻代增加更多的Region,但最多年轻代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent”调整。年轻代中的Eden和Survivor对应的region也跟之前一样,默认8:1:1,假设年轻代现在有1000个region,eden区对应800个,s0对应100个,s1对应100个。
但需要注意的是Region在G1中是动态的,一个Region一开始可能是年轻代,但是当垃圾回收空闲出来后,它下次可能继续作为年轻代使用,也有可能被作为老年代或Humongous使用。也就是G1中的分代,实际上是逻辑上的分代。
G1中对象什么时候进行老年代与我们之前介绍过的是一样的,唯一不同的是大对象的处理,G1中有专门分配大对象的Region叫Humongous区,当一个对象超过Region大小的50%,我们就会认为其是大对象,并将其放到Humongous区。比如,按照上面所说,每个Region是2MB,那么只要对象超过了1MB,就会被放入到Humongous,如果一个对象太多,一个Humongous放不下,就会横跨多个Region来存放。
利用Humongous专门来存放短期的大对象,而不是进入老年代,可以节约老年代的空间,避免因为老年代空间不足而触发GC。

G1的垃圾回收过程与CMS极为相似,大致可以分为几个步骤:
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来),比如一个Region花200ms能回收10M垃圾,另外一个Region花50ms能回收20M垃圾,在回收时间有限情况下,G1当然会优先选择后面这个Region回收。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率。
不熟悉gc roots和可达性分析的可以看这篇文章https://blog.csdn.net/qq_32099833/article/details/109253339关于漏标问题如果不清楚,可以看这篇文章https://www.cnblogs.com/hongdada/p/14578950.html
我认为G1最大的特点就是可预测的停顿时间,G1会将垃圾回收停顿的时间控制在-XX:MaxGCPauseMillis指定的值。
不过, 这里设置的“期望值”必须是符合实际的, 不能异想天开, 毕竟G1是要冻结用户线程来复制对象的, 这个停顿时间再怎么低也得有个限度。 它默认的停顿目标为两百毫秒, 一般来说, 回收阶段占到几十到一百甚至接近两百毫秒都很正常, 但如果我们把停顿时间调得非常低, 譬如设置为二十毫秒, 很可能出现的结果就是由于停顿目标时间太短, 导致每次选出来的回收集只占堆内存很小的一部分, 收集器收集的速度逐渐跟不上分配器分配的速度, 导致垃圾慢慢堆积。 很可能一开始收集器还能从空闲的堆内存中获得一些喘息的时间, 但应用运行时间一长就不行了, 最终占满堆引发Full GC反而降低性能, 所以通常把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的。
G1并不是Eden区放满了就会马上进行young gc,而是会计算一下目前回收Eden需要多少时间,如果回收时间远小于-XX:MaxGCPauseMillis设置的值,那么就会增加新的年轻代Region,直到下次Eden区再次放满,且回收Eden区的停顿时间接近-XX:MaxGCPauseMillis设置的值,此时才会触发young gc
mixed gc并不是full gc,当G1老年代的占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有年轻代和部分老年代(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC
停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。因此我们要极力避免发生full gc
举个例子,像Kafka这样的支撑高并发消息系统大家肯定不陌生,对于kafka来说,每秒处理几万甚至几十万消息时很正常的,一般来说部署kafka需要用大内存机器(比如64G),也就是说可以给年轻代分配个三四十G的内存用来支撑高并发处理,这里就涉及到一个问题了,我们以前常说的对于eden区的young gc是很快的,这种情况下它的执行还会很快吗?很显然,不可能,因为内存太大,处理还是要花不少时间的,假设三四十G内存回收可能最快也要几秒钟,按kafka这个并发量放满三四十G的eden区可能也就一两分钟吧,那么意味着整个系统每运行一两分钟就会因为young gc卡顿几秒钟没法处理新消息,显然是不行的。那么对于这种情况如何优化了,我们可以使用G1收集器,设置 -XX:MaxGCPauseMills 为50ms,假设50ms能够回收三到四个G内存,然后50ms的卡顿其实完全能够接受,用户几乎无感知,那么整个系统就可以在卡顿几乎无感知的情况下一边处理业务一边收集垃圾。
G1天生就适合这种大内存机器的JVM运行,可以比较完美的解决大内存垃圾回收时间过长的问题。
最后简单总结一下这篇文章的主要内容: