同优秀的CMS垃圾回收器一样,
- G1也是关注最小时延的垃圾回收器,
- 也同样适合大尺寸堆内存的垃圾收集,
- 官方也推荐使用G1来代替选择CMS。
G1最大的特点是引入分区的思路,
- 弱化了分代的概念,
- 合理利用垃圾收集各个周期的资源,
- 解决了其他收集器甚至CMS的众多缺陷。
串行收集器
- 串行收集器组合 Serial + Serial Old
- 开启选项:
-XX:+SerialGC
- 串行收集器是最基本、发展时间最长、的垃圾收集器,
- 也是client模式下的默认收集器配置。
- 串行收集器采用单线程stop-the-world(STW)的方式进行收集。
- 当内存不足时,
- 串行GC设置停顿标识,待所有线程都进入安全点(Safepoint)时,
- 应用线程暂停,串行GC开始工作,
- 采用单线程方式回收空间并整理内存。
- 单线程也意味着
- 复杂度更低、
- 占用内存更少,
- 但同时也意味着不能有效利用多核优势。
- 事实上,串行收集器特别适合堆内存不高、单核甚至双核CPU的场合。
- 串行GC设置停顿标识,待所有线程都进入安全点(Safepoint)时,
- 当内存不足时,
并行收集器
- 并行收集器组合 Parallel Scavenge + Parallel Old
- 开启选项:
-XX:+UseParallelGC
或-XX:+UseParallelOldGC
(可互相激活) - 并行收集器是以关注吞吐量为目标的垃圾收集器
- 也是server模式下的默认收集器配置,
- 对吞吐量的关注主要体现在年轻代Parallel Scavenge收集器上。
并发标记清除收集器
- 并发标记清除收集器组合 ParNew + CMS + Serial Old
- 开启选项:
-XX:+UseConcMarkSweepGC
- 并发标记清除(CMS)是以关注延迟为目标、
- 十分优秀的垃圾回收算法,
- 开启后,年轻代使用STW式的并行收集,
- 老年代回收采用CMS进行垃圾回收,
- 对延迟的关注也主要体现在老年代CMS上。
- 年轻代ParNew与并行收集器类似,
- 而老年代CMS每个收集周期都要经历:
- 初始标记、
- 以STW的方式标记所有的根对象
- 并发标记、
- 并发标记则同应用线程一起并行,标记出根对象的可达路径;
- 重新标记、
- 标记那些应用线程修改而引起的可能错过的可达对象
- 并发清除。
- 最后得到的不可达对象将在并发清除阶段进行回收
- 值得注意的是,初始标记和重新标记都已优化为多线程执行
- 初始标记、
- CMS非常适合堆内存大、
- CPU核数多的服务器端应用,
- 也是G1出现之前大型应用的首选收集器。
- 但是CMS并不完美,它有以下缺点:
- (1)由于并发进行,
- CMS在收集与应用线程会同时会增加对堆内存的占用,
- 也就是说,CMS必须要在老年代堆内存用尽之前完成垃圾回收,
- 否则CMS回收失败时,将触发担保机制,
- 串行老年代收集器将会以STW(stop-the-world)的方式进行一次GC,从而造成较大停顿时间;
- CMS在收集与应用线程会同时会增加对堆内存的占用,
- (2)标记清除算法无法整理空间碎片,
- 老年代空间会随着应用时长被逐步耗尽,
- 最后将不得不通过担保机制对堆内存进行压缩。
- CMS也提供了参数
-XX:CMSFullGCsBeForeCompaction
(默认0,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC。
- 老年代空间会随着应用时长被逐步耗尽,
- (1)由于并发进行,
Garbage First
- Garbage First (G1)
- 开启选项:
-XX:+UseG1GC
- 开启选项:
- 之前介绍的几组垃圾收集器组合,都有几个共同点:
- 年轻代、老年代是独立且连续的内存块;
- 年轻代收集使用单eden、双survivor进行复制算法;
- 老年代收集必须扫描整个老年代区域;
- 都是以尽可能少而快地执行GC为设计原则。
- G1垃圾收集器也是
- 以关注延迟为目标、
- 服务器端应用的垃圾收集器,
- 被HotSpot团队寄予取代CMS的使命
- G1也有类似CMS的收集动作:
- 初始标记、并发标记、重新标记、清除、转移回收
- 并且也以一个串行收集器做担保机制
- G1收集与以上三组收集器有很大不同:
- G1的设计原则是"首先收集尽可能多的垃圾(Garbage First)"。
- G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,
- 而是在内部采用了启发式算法,
- 在老年代找出具有高收集收益的分区进行收集。
- 同时G1可以根据用户设置的暂停时间目标自动调整年轻代和总堆大小,
- 暂停目标越短年轻代空间越小、总空间就越大;
- G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,
- G1采用内存分区(Region)的思路,
- 将内存划分为一个个相等大小的内存分区,
- 回收时则以分区为单位进行回收,
- 存活的对象复制到另一个空闲分区中。
- 由于都是以相等大小的分区为单位进行操作,
- 因此G1天然就是一种压缩方案(局部压缩);
- 将内存划分为一个个相等大小的内存分区,
- G1虽然也是分代收集器,
- 但整个内存分区不存在物理上的年轻代与老年代的区别,
- 也不需要完全独立的survivor(to space)堆做复制准备。
- G1只有逻辑上的分代概念,
- 或者说每个分区都可能随G1的运行在不同代之间前后切换;
- G1的收集都是STW(stop-the-world)的,
- 但年轻代和老年代的收集界限比较模糊,
- 采用了混合(mixed)收集的方式。
- 即每次收集既可能只收集年轻代分区(年轻代收集),
- 也可能在收集年轻代的同时,包含部分老年代分区(混合收集),
- 这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。
- G1的设计原则是"首先收集尽可能多的垃圾(Garbage First)"。
G1的内存模型
-
分区概念
-
分区
- G1采用了分区(Region)的思路,
- 将整个堆空间分成若干个大小相等的内存区域,
- 每次分配对象空间将逐段地使用内存。
- 因此,在堆的使用上,
- G1并不要求对象的存储一定是物理上连续的,
- 只要逻辑上连续即可;
- 每个分区也不会确定地为某个代服务,
- 可以按需在年轻代和老年代之间切换。
- 启动时可以通过参数
-XX:G1HeapRegionSize=n
- 可指定分区大小(1MB~32MB,且必须是2的幂),
- 默认将整堆划分为2048个分区。
- G1采用了分区(Region)的思路,
-
卡片
- 在每个分区内部又被分成了若干个大小为512 Byte卡片(Card),
- 堆内存最小可用粒度
- 所有分区的卡片将会记录在全局卡片表(Global Card Table)中,
- 分配的对象会占用物理上连续的若干个卡片,
- 当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见RSet)。
- 每次对内存的回收,都是对指定分区的卡片进行处理。
- 在每个分区内部又被分成了若干个大小为512 Byte卡片(Card),
-
堆
- G1同样可以通过-Xms/-Xmx来指定堆空间大小。
- 当发生年轻代收集或混合收集时,
- 通过计算GC与应用的耗费时间比,
- 自动调整堆空间大小。
- 如果GC频率太高,则通过增加堆尺寸,来减少GC频率,
- 相应地GC占用的时间也随之降低;
- 目标参数-XX:GCTimeRatio即为GC与应用的耗费时间比,
- G1默认为9,而CMS默认为99,
- 因为CMS的设计原则是耗费在GC上的时间尽可能的少。
- 另外,当空间不足,如对象空间分配或转移失败时,
- G1会首先尝试增加堆空间,
- 如果扩容失败,则发起担保的Full GC。
- Full GC后,堆尺寸计算结果也会调整堆空间。
-
分代模型
-
分代
- G1将内存在逻辑上划分为年轻代和老年代,
- 其中年轻代又划分为Eden空间和Survivor空间。
- 但年轻代空间并不是固定不变的,
- 当现有年轻代分区占满时,JVM会分配新的空闲分区加入到年轻代空间。
- 整个年轻代内存会在初始空间-XX:G1NewSizePercent(默认整堆5%)与最大空间-XX:G1MaxNewSizePercent(默认60%)之间动态变化,
- 且由参数目标暂停时间-XX:MaxGCPauseMillis(默认200ms)、需要扩缩容的大小以及分区的已记忆集合(RSet)计算得到。
- 当然,G1依然可以设置固定的年轻代大小(参数-XX:NewRatio、-Xmn),但同时暂停目标将失去意义。
- G1将内存在逻辑上划分为年轻代和老年代,
-
本地分配缓冲
- 本地分配缓冲 Local allocation buffer (Lab)
- 由于分区的思想,
- 每个线程均可以"认领"某个分区用于线程本地的内存分配,
- 而不需要顾及分区是否连续。
- 因此,每个应用线程和GC线程都会独立的使用分区,
- 进而减少同步时间,提升GC效率,这个分区称为本地分配缓冲区(Lab)。
- 其中,应用线程可以独占一个本地缓冲区(TLAB)来创建的对象,
- 而大部分都会落入Eden区域(巨型对象或分配失败除外),
- 因此TLAB的分区属于Eden空间;
- 而每次垃圾收集时,每个GC线程同样可以独占一个本地缓冲区(GCLAB)用来转移对象,
- 每次回收会将对象复制到Suvivor空间或老年代空间;
- 对于从Eden/Survivor空间晋升(Promotion)到Survivor/老年代空间的对象,
- 同样有GC独占的本地缓冲区进行操作,
- 该部分称为晋升本地缓冲区(PLAB)。
-
分区模型
- G1对内存的使用以分区(Region)为单位,而对对象的分配则以卡片(Card)为单位。
-
巨型对象
- 巨型对象 Humongous Region
- 一个大小达到甚至超过分区大小一半的对象称为巨型对象(Humongous Object)。
- 因为巨型对象的移动成本很高,
- 而且有可能一个分区不能容纳巨型对象。
- 因此,巨型对象会直接在老年代分配,
- 所占用的连续空间称为巨型分区(Humongous Region)。
- 巨型对象会独占一个、或多个连续分区,
- 其中第一个分区被标记为开始巨型(StartsHumongous),
- 相邻连续分区被标记为连续巨型(ContinuesHumongous)。
- 巨型对象 Humongous Region
-
已记忆集合
- 已记忆集合 Remember Set (RSet)
- 在串行和并行收集器中,
- GC通过整堆扫描,来确定对象是否处于可达路径中
- G1为了避免STW式的整堆扫描,
- 在每个分区记录了一个已记忆集合(RSet),
- 内部类似一个反向指针,
- 记录引用分区内对象的卡片索引。
- 当要回收该分区时,通过扫描分区的RSet,
- 来确定引用本分区内的对象是否存活,
- 进而确定本分区内的对象存活情况。
- 事实上,并非所有的引用都需要记录在RSet中,
- 如果一个分区确定需要扫描,那么无需RSet也可以无遗漏的得到引用关系。
- 那么引用源自本分区的对象,当然不用落入RSet中;
- 同时,G1 GC每次都会对年轻代进行整体收集,因此引用源自年轻代的对象,也不需要在RSet中记录。
- 最后只有老年代的分区可能会有RSet记录,
- 这些分区称为拥有RSet分区(an RSet’s owning region)。
- 如果一个分区确定需要扫描,那么无需RSet也可以无遗漏的得到引用关系。
-
Per Region Table
- RSet在内部使用Per Region Table(PRT)记录分区的引用情况
- 由于RSet的记录要占用分区的空间,
- 如果一个分区非常"受欢迎",
- 那么RSet占用的空间会上升,从而降低分区的可用空间。
- G1应对这个问题采用了改变RSet的密度的方式
- 在PRT中将会以三种模式记录引用:
- 稀少:直接记录引用对象的卡片索引
- 细粒度:记录引用对象的分区索引
- 粗粒度:只记录引用情况,每个分区对应一个比特位
- 粗粒度的PRT只是记录了引用数量,
- 需要通过整堆扫描才能找出所有引用,
- 因此扫描速度也是最慢的。
- 在PRT中将会以三种模式记录引用:
-
收集集合 (CSet)
- 收集集合(CSet)代表每次GC暂停时回收的一系列目标分区。
-
年轻代收集集合
- 年轻代收集集合 CSet of Young Collection
- 当JVM分配对象到Eden区域失败(Eden区已满)时,
- 便会触发一次STW式的年轻代收集。
- 在年轻代收集中,
- Eden分区存活的对象将被拷贝到Survivor分区;
- 原有Survivor分区存活的对象,
- 将根据任期阈值(tenuring threshold)分别晋升到PLAB中,
- 新的survivor分区和老年代分区。
- 而原有的年轻代分区将被整体回收掉。
- 同时,年轻代收集还负责维护对象的年龄(存活次数),
- 辅助判断老化(tenuring)对象晋升的时候是到Survivor分区还是到老年代分区。
- 年轻代收集首先先将晋升对象尺寸总和、对象年龄信息维护到年龄表中,再根据年龄表、Survivor尺寸、Survivor填充容量-XX:TargetSurvivorRatio(默认50%)、最大任期阈值-XX:MaxTenuringThreshold(默认15),
- 计算出一个恰当的任期阈值,凡是超过任期阈值的对象都会被晋升到老年代。
-
混合收集集合
- 混合收集集合 CSet of Mixed Collection
- 当老年代占用空间超过整堆比IHOP阈值
-XX:InitiatingHeapOccupancyPercent
(默认45%)时,- G1就会启动一次混合垃圾收集周期。
- 为了满足暂停目标(暂停的时间限制),
- G1可能不能一口气将所有的候选分区收集掉,
- 因此G1可能会产生连续多次的混合收集与应用线程交替执行,
- 每次STW的混合收集与年轻代收集过程相类似。