G1概述
应用程序所应对的业务越来越庞大,复杂,用户越来越多,没有GC就不能保证应用程序正常运行,而经常赞成STW的GC又跟不上十几的需求,所以才会不断地尝试对GC进行优化。G1(Garbage First)垃圾回收器在JDK7引入的一个新的垃圾回收器,也是在JDK9中默认垃圾收集器,是当今收集器技术发展最前沿成果之一。
同时,为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间,同时兼顾良好的吞吐量。官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量。
G1是一个并行回收器,它把堆内存风格为很多不相关的区域(region,物理上不连续)。使用不同的region来表示Eden,S0,S1,Old区域等。
G1有计划地避免在整个Java堆中进行全区域的垃圾收集,G1跟踪各个region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的region。
由于这种方式的侧重点在于回收垃圾最大量的区间,所以我们给G1一个名字:垃圾优先。
G1是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征。
G1的特点(优势)
G1垃圾回收思想和之前的并不一样,如下图:
而是将空间分成了很多小的区间(region)
并行与并发:
- 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW.
- 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段完全阻塞应用程序的情况。
分代收集:
- 从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和幸存者区。但从堆的结构上看,他不要求整个Eden区,年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。
- 将堆空间分为若干个区域(region),这些去榆中包含了逻辑上的年轻代和老年代。
- 和之前的各类回收器不同,它同时兼顾年轻代和老年代,对比其他回收器,或者工作在年轻代,或者工作在老年代。
空间整合
- G1将内存会分为一个个的region。内存的回收是以region作为基本单位的。region之间是复制算法,但整体上实际可以看作是标记压缩算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显。
可预测的停顿时间模型
这是G1相对CMS的另一大优势,G1除乐追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒。
- 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局挺短的发生也能得到较好的控制。
- G1跟踪各个region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
- 相比CMS,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。
G1的缺点
相较于CMS,G1还不具备全方位,压倒性优势。在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比CMS要高。
从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用商店则发挥其优势。平衡点在6-8G之间。
G1的参数设置
G1的适用场景
分区region:化整为零
使用G1收集器时,它将整个Java堆划分为约2048个大小相同的独立region块,每个region块大小根据堆空间实际大小而定,整体被控制咋1M-32M之间,且为2的N次幂,即1M,2M,4M,8M,16M,32M。可以通过-XX:G1HeapRegionSize设定。所有的region大小相同,且在JVM生命周期内不会被改变。
虽然还保留新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,他们都是一部分region的集合。通过region的动态分配方式实现逻辑上的连续。
一个region有可能属于Eden,幸存者,或者老年代内存区域。但是一个region只可能属于一个角色,图中E表示该region属于Eden内存区域,S表示属于幸存者内存区域,O表示属于Old区域。空白表示未使用的内存空间。
G1垃圾收集器还增加了一种新的内存区域,叫做humongous内存区域,如图中H块。主要用于存储大对象,如果超过1.5个region,就放在H。
对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个humongous区,它用来专门存大对象。如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。为了能找到连续H区,有时候不得不启动Full GC,G1的大多数行为都把H区作为老年代的一部分来看待。
记忆集和写屏障
我们应该清楚,一个region不可能试试孤立的,一个region中的对象可能被其他任意region的对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确? 这样就会出现回收新生代时也不得不同时扫描老年代,如果这样就会降低新生代GC的效率。
无论G1还是其他分代收集器,JVM都是使用记忆集来避免全局扫描的:
每个region都有一个对应的记忆集,每次引用类型数据写操作时,都会产生一个写屏障暂时中断操作。然后检查将要写入的引用指向的对象是否和该引用类型数据在不同的region(其他收集器检查老年代对象是否引用了新生代对象)。如果不同,通过卡表吧相关引用信息记录到引用指向对象所在的region对应的记忆集中。当进行垃圾收集时,在GC根节点枚举范围假如记忆集,就可以保证不进行全局扫描,也不会有遗漏。
G1回收器垃圾回收过程
G1回收过程包括以下环节:
- 年轻代GC
- 来年代并发标记过程
- 混合回收
- 如果需要,单线程,独占式,高强度Full GC还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收。
应用程序分配内存,当年清代的Eden区用尽时开始年轻代回收过程;G1的年轻代手机阶段是一个并行的独占式收集器。在年轻代回收期,G1暂停所有应用线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到幸存者区或者老年区,也有可能是两个区间都会涉及。
当堆内存使用达到一定值(默认45%),开始老年代并发标记过程。
标记完成马上开始混合回收过程。对于一个混合回收期,G1从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分,和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代回收,一次只需要扫描部分老年代的region就可以了,同时这个老年代region和年轻代一起被回收的。
年轻代GC
JVM启动时,G1先准备好伊甸园区,程序在运行过程中不断创建对象到伊甸园区,当伊甸园区空间耗尽时,G1会启动一次年轻代垃圾回收过程。
年轻代GC时,首先G1停止应用程序的执行(STW),G1创建回收集,回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含伊甸园区和幸存者区所有的内存分段。
然后开始如下回收过程:
第一阶段:扫描根
根是指static变量指向的对象,正在执行的方法调用链条上的局部变量等。根引用连同记忆集记录的外部引用作为扫描存活对象的入口。
第二阶段:更新记忆集
处理dirty card queue 中的card,更新记忆集。此阶段完成后,记忆集可以准确的反应老年代对所咋的内存分段中对象的引用。
第三阶段:处理记忆集
识别被老年代对象指向的伊甸园中的对象,这些被指向的伊甸园中的对象被认为是存活的对象。
第四阶段:复制对象
此阶段,对象数被遍历,伊甸园区内存段中存活的对象会被复制到幸存者区中空的内存段,幸存者区内存段中存活的对象如果年龄没有达到阈值,年龄会+1,达到阈值会被复制到养老区中空的内存段。如果幸存者空间不够,伊甸园空间的部分数据会直接晋升到老年代空间。
第五阶段:处理引用
处理强软弱虚等引用,最终伊甸园空间的数据为空,GC停止工作,而目标内存中的对象都是联系存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。
并发标记过程
初始化阶段
标记从根节点直接可达对象,这个阶段是STW的,并且会触发一次年轻代GC.
根区域扫描
G1 扫描幸存者区直接可达的老年代区域对象,并标记被引用的对象,这一过程必须在年轻代GC之前完成。
并发标记
在整个堆中进行并发标记(和应用程序并发执行),此过程可能被年轻代GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性(去榆中存活对象的比例)。
在此标记
由于应用程序进行,需要修正上一次的标记结果。是STW的。G1中采用了比CMS更快的初始快照算法。
独占清理
计算各个区域的存活对象和GC回收比例。并进行排序,识别可以混合回收的区域,为下阶段做铺垫,是STW的,这个阶段并不会实际上去做垃圾的收集。
并发清理阶段
识别并清理完全空闲的区域。
混合回收
当越来越多的对象晋升到老年大old region时,为了避免堆内存耗尽,虚拟机会触发一个混合的垃圾收集器,该算法并不是一个Old GC,除了回收整个年轻region,还会回收一部分old region。