区域化分代式垃圾回收器
简单介绍G1 GC
官方给G1垃圾收集器设定的目标:
在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起"全功能收集器"的重任与期望
G1是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具吞吐量的性能特征。
为什么名字叫做Garbage First 呢?
1.G1是一个并行的回收器,它把堆内存分割为很多不想关的区域(region)(物理上不连续的)使用不同的region来表示Eden,幸存者0区,幸存者1区,老年代等
2.G1 GC有计划的避免在整个java堆进行全区域的垃圾收集,G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的region
3.由于这种方式的侧重点在于回收垃圾最大量的区间(region),所以我们取名为垃圾优先 Garbage First
G1回收器的特点(优点)
并行并发
1.并行性: G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW
2.并发性: G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
分代收集
1.从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,他不要求整个Eden区,年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量
2.将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代
3.和之前的各类回收器不同,它同时兼顾年轻代和老年代。
空间整合
1.G1将内存划分为region,内存的回收以region为单位,region之间是复制算法,但是整体上可看作是标记-压缩算法,这两种算法都可以避免内存碎片,这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其当堆非常大的时候,G1的优势很明显
可预测的时间停顿模型(即:软实时soft real-time)
这是G1相对CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒
1. 由于分区了,G1可以每次只选取部分区域进行回收,缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制
2.G1跟踪多个region里面的垃圾堆积的价值大小(回收能获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的region,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率
3.相比CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多
4.为啥叫软实时,因为G1不能确保垃圾回收的停顿时间一定在你的设置时间之内,只能尽量确保,所以是软实时
G1垃圾回收器的缺点
1相较于CMS,G1并没有具备全方位碾压优势,比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比CMS要高(G1会有Remembered Set 等额外内存占用,大概是10到20),从经验上来说,在小内存上CMS的表现大概率超过G1,而G1在大内存应用上能发挥它的优势,平衡点在6-8G之间
G1回收器的参数设置
-XX:+UseG1GC
手动指定使用G1收集器
-XX:G1HeapRegionSize
设置每个Region的大小,值是2的幂,范围是1到32MB,目标是根据最小的java堆大小划分出约2048个区域,默认是堆内存的1/2000
-XX:MaxGCPauseMillis
设置期望达到的最大GC停顿时间指标(JVM会尽力实现,并不保证达到) 默认是200ms
-XX:ParallelGCThread
设置STW工作线程数的值,最多设置为8
-XX:ConcGCThreads
设置并发标记的线程数,一般设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右
-XX:InitiatingHeapOccupancyPercent
设置触发并发GC周期的Java堆占用率阈值,超过此值,就触发GC,默认45
G1回收器的适用场景
面向服务端应用,针对具有大内存,多处理器的机器。最主要就是需要低延迟,具有大内存堆的应用程序
比如在堆大小约6G或者更大的时候,可预测的停顿时间低于0.5秒(G1通过每次只清理一部分而不是全部region的增量式清理来保证每次GC的停顿时间)
对于打算从CMS或者ParallelOld收集器迁移过来的应用,按照官方 的建议,如果发现符合如下特征,可以考虑更换成G1收集器以追求更佳性能:
1.超过50%的java堆被活动数据占用
2.对象分配频率或者年代提升频率变化很大
3.GC停顿时间过长(长于0.5秒至1秒)
HotSpot垃圾收集器里,除了G1以外,其他的垃圾收集器使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用线程承担后台运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程
Region介绍
G1收集器会将整个堆划分为一个个的region 块,所有的region块大小大同,且在JVM生命周期内不会被改变。
虽然还保留了新生代和老年代的概念,但不再是物理隔离的了,都是一部分region(不需要连续)的集合,通过region动态分配方式 实现逻辑上的连续
设置H大对象内存区域的原因
对于大对象,默认一般都是直接分配到老年代了,但是如果它是一个短期存在的大对象,放在老年代,老年代因为回收频率很低,导致它长期存在,类似内存泄漏了,不太好,所以专门划分了一个Humongous区,专门存放大对象,如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储,为了找到连续的H区,有时候会不得不启动Full GC ,G1的大多数行为都把H区作为老年代的一部分来看待
G1回收器的垃圾回收过程
1.年轻代GC(Young GC)
2.老年代并发标记过程(Concurrent Marking)
3.混合回收(Mixed GC)
4(如果需要,单线程,独占式,高强度的Full GC还是继续存在的,它针对GC的评估失败提供了一种失败保护机制,即强力回收)
记忆集和写屏障
一个对象被不同区域引用的问题
一个region不可能是孤立,可能被其他任意region中的对象所引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确?
其他收集器也有这个问题,G1更突出一点,因为G1针对大堆
难道回收年轻代一定得扫描老年代吗?那还如何保证效率呢?
解决办法
1.无论是G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描
2.每个region都有一个对应的R Set
3.每次引用类型数据写操作时,都会产生一个Write Barrier(写屏障)暂时中断操作,然后检查将要写入的引用指向的对象是否和该引用类型数据在不同的Region(其他收集器也一样,检查老年代对象是否引用了新生代对象),如果不在同一个region,通过CardTable把相关引用信息记录到引用指向对象的所在region对应的R Set中
4.当进行垃圾收集的时候,在GC根节点的枚举范围内加入R Set ,就可以保证不进行全局扫描,也不会遗漏了
G1垃圾回收的具体过程
过程1:年轻代GC
上图有个不准确的地方,Eden区回收之后,就应该变为空闲区域了
针对第二阶段补充说明
对于一个操作 例如 Object.field = Object JVM会在之前和之后进行特殊操作以在dirty card queue中入队一个保存了对象引用信息的card,在年轻代回收的时候,G1会为Dirty Card Queue 中所有的card进行处理,以更新R Set 保证R Set能实时准确的反映引用关系
为什么不在引用赋值的时候直接更新RSet呢,考虑到了性能的问题,RSet的处理需要线程同步,开销很大,使用队列性能好一点
针对第五阶段说明
回收之后,空闲的region 会被一个linkedList管理起来,进行动态分配
过程2:并发标记阶段
另一种说法
过程3:混合回收
过程4:Full GC
G1回收器优化建议
收集器大总结