0. 前言
G1是JDK7-u4推出商用的垃圾收集器,早在JDK 6u14就有Early Access版本的G1收集器供开发人员实验、试用,而在最近的JDK9中已经把G1设置为默认的垃圾收集器,下面总结了G1收集器的要点
1. 概述
G1(Garbage First)垃圾收集器是当今垃圾回收技术最前沿的成果之一。同优秀的CMS垃圾回收器一样,G1也是关注最小时延的垃圾回收器,也同样适合大尺寸堆内存的垃圾收集,官方也推荐使用G1来代替选择CMS。G1最大的特点是引入分区的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷。
2. 特点
- 并行与并发
能充分利用多CPU、多核环境下的硬件优势,并行来缩短”Stop The World”停顿时间,也可以并发让垃圾收集与用户程序同时进行。 - 分代收集
收集范围包括新生代和老年代,能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;能够采用不同方式处理不同时期的对象。虽然保留分代概念,但Java堆的内存布局有很大差别;将整个堆划分为多个大小相等的独立区域(Region),新生代和老年代不再是物理隔离,它们都是一部分Region(逻辑连续)的集合。 - 空间整合
与CMS收集器的标记-清除算法不同,结合了多种垃圾收集算法,从整体看,是基于标记-整理算法;从局部(两个Region间)看,是基于复制算法,都不会产生内存碎片,有利于长时间运行。 - 可预测的停顿
低停顿的同时实现高吞吐量,G1除了追求低停顿处,还能建立可预测的停顿时间模型;可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒。
设计原则
G1的设计原则是”首先收集尽可能多的垃圾(Garbage First)”。因此,G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部采用了启发式算法:在老年代找出具有高收集收益的分区进行收集(回收所获得空间大小和回收所需要时间的经验值)。同时G1可以根据用户设置的暂停时间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间就越大;
3. 运作步骤
不计算维护Remembered Set的操作,可以分为4个步骤(与CMS较为相似)。
- 初始标记(Initial Marking)
仅标记一下GC Roots能直接关联到的对象,且修改TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的Region中创建新对象,需要”Stop The World”,但速度很快。 - 并发标记(Concurrent Marking)
进行GC Roots 可达性分析的过程;刚才产生的集合中标记出存活对象,耗时较长,但可以和应用程序也并发运行;并不能保证可以标记出所有的存活对象。 - 最终标记(Final Marking)
为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录,上一阶段对象的变化记录在线程的Remembered Set Log;这里把Remembered Set Log合并到Remembered Set中;需要”Stop The World”,且停顿时间比初始标记稍长,但远比并发标记短;可采用多线程并行执行来提升效率。 - 筛选回收(Live Data Counting and Evacuation)
首先排序各个Region的回收价值和成本,然后根据用户期望的GC停顿时间来制定回收计划,最后按计划回收一些价值高的Region中垃圾对象。回收时采用”复制”算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;可以和应用程序并发进行,但停顿用户线程可以大幅度提高回收效率。
G1收集器运行示意图如下:
更加详细的示意图如下:
4. 收集模式和收集时机
G1收集模式分为两种:每次收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。
年轻代收集
应用线程不断活动后,年轻代空间会被逐渐填满。当JVM分配对象到Eden区域失败(Eden区已满)时,便会触发一次STW式的年轻代收集。在年轻代收集中,Eden分区存活的对象将被拷贝到Survivor分区;原有Survivor分区存活的对象,将根据任期阈值(tenuring threshold)分别晋升到PLAB中,新的survivor分区和老年代分区。而原有的年轻代分区将被整体回收掉。
混合收集
年轻代收集不断活动后,老年代的空间也会被逐渐填充。当老年代占用空间超过整堆比IHOP阈值-XX:InitiatingHeapOccupancyPercent
(默认45%)时,G1就会启动一次混合垃圾收集周期(发起并发标记周期,即之前详细示意图中的Concurrent Marking周期)。为了满足暂停目标,G1可能不能一口气将所有的候选分区收集掉,因此G1可能会产生连续多次的混合收集与应用线程交替执行,每次STW的混合收集与年轻代收集过程相类似。
事实上,当达到IHOP阈值时,进行混合收集,G1开始着手准备收集老年代空间。首先经历并发标记周期,识别出高收益的老年代分区,前文已述。但随后G1并不会马上开始一次混合收集,而是让应用线程先运行一段时间,等待触发一次年轻代收集。在这次STW中,G1将保准整理混合收集周期。接着再次让应用线程运行,当接下来的几次年轻代收集时,将会有老年代分区加入到CSet中,即触发混合收集,这些连续多次的混合收集称为混合收集周期(Mixed Collection Cycle)。
5. 重要概念
关于G1的一些重要概念的详细讲解。
5.1 分区 Region
G1采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。因此,在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可;每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize=n
可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。
5.2 卡片 Card
在每个分区内部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见RSet)。每次对内存的回收,都是对指定分区的卡片进行处理。
5.3 已记忆集合 Remember Set (RSet)
在串行和并行收集器中,GC通过整堆扫描,来确定对象是否处于可达路径中。然而G1为了避免STW式的整堆扫描,在每个分区记录了一个已记忆集合(RSet)。当要回收该分区时,通过扫描分区的RSet,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况。
关于一个对象被不同区域引用的问题:一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确?
解决方法:每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作,然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象),如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中,当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set,就可以保证不进行全局扫描,也不会有遗漏。
5.4 收集集合 Collection Set (CSet)
收集集合(CSet)代表每次GC暂停时回收的一系列目标分区。在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。因此无论是年轻代收集,还是混合收集,工作的机制都是一致的。年轻代收集CSet只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到CSet中。
候选老年代分区的CSet准入条件,可以通过活跃度阈值-XX:G1MixedGCLiveThresholdPercent
(默认85%)进行设置,从而拦截那些回收开销巨大的对象;同时,每次混合收集可以包含候选老年代分区,可根据CSet对堆的总大小占比-XX:G1OldCSetRegionThresholdPercent
(默认10%)设置数量上限。
由上述可知,G1的收集都是根据CSet进行操作的,年轻代收集与混合收集没有明显的不同,最大的区别在于两种收集的触发条件。
5.5 巨型对象 Humongous Region
一个大小达到甚至超过分区大小一半的对象称为巨型对象(Humongous Object)。当线程为巨型分配空间时,不能简单在年轻代进行分配,因为巨型对象的移动成本很高,而且有可能一个分区不能容纳巨型对象。因此,巨型对象会直接在老年代分配,所占用的连续空间称为巨型分区(Humongous Region)。G1内部做了一个优化,一旦发现没有引用指向巨型对象,则可直接在年轻代收集周期中被回收。
巨型对象会独占一个、或多个连续分区,其中第一个分区被标记为开始巨型(StartsHumongous),相邻连续分区被标记为连续巨型(ContinuesHumongous)。由于无法享受Lab带来的优化,并且确定一片连续的内存空间需要扫描整堆,因此确定巨型对象开始位置的成本非常高,如果可以,应用程序应避免生成巨型对象。
6.转移失败的担保机制
转移失败(Evacuation Failure)是指当G1无法在堆空间中申请新的分区时,G1便会触发担保机制,执行一次STW式的、单线程的Full GC。Full GC会对整堆做标记清除和压缩,最后将只包含纯粹的存活对象。参数-XX:G1ReservePercent
(默认10%)可以保留空间,来应对晋升模式下的异常情况,最大占用整堆50%,更大也无意义。
G1在以下场景中会触发Full GC,同时会在日志中记录to-space-exhausted以及Evacuation Failure:
- 从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
- 从老年代分区转移存活对象时,无法找到可用的空闲分区
- 分配巨型对象时在老年代无法找到足够的连续分区
由于G1的应用场合往往堆内存都比较大,所以Full GC的收集代价非常昂贵,应该避免Full GC的发生。
7.JVM相关参数
No | 参数名称 | 描述 |
---|---|---|
1 | -XX:UseG1GC | 开启G1收集器,JDK9默认开启 |
2 | -XX:G1HeapRegionSize=n | 分区(Region)大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。 |
3 | -XX:MaxGCPauseMills=n | 回收的最大时间,默认200ms |
4 | -XX:G1NewSizePercent=n | 年轻代最小使用的空间比率,默认为Java堆内存的5% |
5 | -XX:G1MaxNewSizePercent=n | 年轻代最大使用的空间比率,默认为Java堆内存的60% |
6 | -XX:ParallelGCThreads=n | STW工作线程数,与CPU的数量有关 |
7 | -XX:ConcGCThreads=n | 并行标记线程数 |
8 | -XX:InitiatingHeapOccupancyPercent =n | 当老年代占用空间超过整堆比IHOP阈值(默认45%)时,G1就会启动一次混合垃圾收集周期 |
9 | -XX:NewRatio=n | 年轻代与老年代比率,默认为2 |
10 | -XX:SurvivorRatio=n | Eden区和Survivor区比率,默认为8 |
11 | -XX:MaxTenuringThreshold=n | 年轻代到老年代的岁数,默认是15,表示控制对象需要15次Minor GC(年轻代GC)才晋升到老年代 |
12 | -XX:G1ReservePercent=n | 设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认是10 |
本笔记参考了《深入理解Java虚拟机:JVM高级特性与最佳实践(第二版)》和以下文章: