垃圾回收器
分类
-
Partial GC:并不收集整个GC堆的模式
- Young GC:只收集young gen的GC,有 Serial GC 、ParNew GC、Parallel Scavenge GC。
- Old GC:只收集old gen的GC。只有 CMS GC 是针对老年代的。
- Mixed GC:收集整个young gen以及部分old gen的GC,即所有分 Region 的收集器,包括 G1、Shenandoah、ZGC。
-
Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式,包括 Serial Old GC、Parallel Old GC 、Full GC for CMS(1 workers of 8,STW) Full GC for G1(1 workers,STW)、Full GC for ZGC(1 workers,STW) 等。
触发条件
- young GC:
- 当 young gen 中的 eden 区分配满的时候触发(注意 young GC 中有部分存活对象会晋升到 old gen,所以 young GC 后 old ge n的占用量通常会有所升高)。
- old GC:
- 当 old/perm gen 被填满,会触发 full gc,在 young gen 使用 young gc 算法,在 old/perm gen 使用 old gc 算法(这点就目前来看,只有 CMS 是单独的老年代收集器,所以指的其使用 -XX:CMSInitiatingOccupancyFraction 设置老年代的触发 CMS 的百分比)。
- full GC:
- 当young gc 发生前,如果发现统计数据说,之前young GC的平均晋升大小 比目前old gen剩余的空间大,会在整个 heap 使用 old gc 算法,不使用 young gc 算法。(CMS 并发失败 或者 碎片太多没有连续空间给一个大对象,都是这种情况);
- 或者System.gc()、heap dump带GC,默认也是触发full GC。
GC 组合
- Serial GC
- Serial Young GC + Serial Old GC (实际上它是全局范围的Full GC);
- +XX:UseSerialGC
- Parallel GC :
- Parallel Young GC + 非并行的 PS MarkSweep GC / 并行的Parallel Old GC(这俩实际上也是全局范围的Full GC)。
- +XX:UseParallalGC(Parallel GC + PS MarkSweep GC)
- +XX:UseParallalOldGC(Parallel GC + Parallel Old GC)
- CMS GC:ParNew GC + CMS GC(Old GC,当 old gen 超过阈值时触发)GC +SerialOld GC(并发失败后备预案,即内存回收的速度赶不上内存分配的速度触发) + Full GC for CMS(出现碎片太多,导致 old gen 无法分配所需空间时触发,开销很大);
- +XX:UseConcMarkSweepGC
- G1 GC:Young GC(eden region + survivor region) + mixed GC(eden region + survivor region + 部分 old region)+ Full GC for G1 GC(若并发失败,即内存回收的速度赶不上内存分配的速度触发 Full GC,开销很大);
- +XX:UseG1GC
新生代
- 下面说的这三种收集器的共性就是:采用分代策略,且新生代都采用 标记-复制算法,所以都必须要 STOP THE WORLD
Serial收集器
缺点
- 这个收集器是一个单线程工作的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线,直到它收集结束。
优点
- 简单而高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint)最小的;
使用场景
- Serial收集器依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器
- 对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
- 在用户桌面的应用场景以及近年来流行的部分微服务应用中,分配给虚拟机管理的内存一般来说并不会特别大,收集几十兆甚至一两百兆的新生代(仅仅是指新生代使用的内存,桌面应用甚少超过这个容量),垃圾收集的停顿时间完全可以控制在十几、几十毫秒,最多一百多毫秒以内,只要不是频繁发生收集,这点停顿时间对许多用户来说是完全可以接受的。所以,Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。
ParNew收集器
和 Serial 收集器关系
- ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。
发展情况
- 除了Serial收集器外,目前只有它能与CMS收集器配合工作,可以说直到CMS的出现才巩固了ParNew的地位。
- 但成也萧何败也萧何,随着垃圾收集器技术的不断改进,更先进的G1收集器带着CMS继承者和替代者的光环登场。所以自JDK 9开始,ParNew加CMS收集器的组合就不再是官方推荐的服务端模式下的收集器解决方案了,官方希望它能完全被G1所取代。并直接取消了-XX:+UseParNewGC参数,这意味着ParNew和CMS从此只能互相搭配使用,即从此以后,ParNew合并入CMS,成为它专门处理新生代的组成部分。ParNew可以说是HotSpot虚拟机中第一款退出历史舞台的垃圾收集器。
使用场景
- 在单核心处理器的环境中绝对不会有比Serial收集器更好的效果,
- 但随着可以被使用的处理器核心数量的增加,ParNew对于垃圾收集时系统资源的高效利用还是很有好处的。它默认开启的收集线程数与处理器核心数量相同,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数
Parallel Scavenge收集器
与 ParNew 异同
- 共同点:同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器,所以 Parallel Scavenge的诸多特性从表面上看和ParNew非常相似。
- 不同点:Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),并且自适应调节策略也是Parallel Scavenge收集器区别于ParNew收集器的一个重要特性。
精确控制吞吐量
-
-XX:MaxGCPauseMillis参数
MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定值。
不过不要异想天开地认为如果把这个参数的值设置得更小一点就能使得系统的垃圾收集速度变得更快,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的:系统把新生代调得小一些,收集300MB新生代肯定比收集500MB快,但这也直接导致垃圾收集发生得更频繁,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
-
-XX:GCTimeRatio参数
GCTimeRatio参数的值则应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。
譬如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5%(即1/(1+19)),默认值为99,即允许最大1%(即1/(1+99))的垃圾收集时间。
自适应的调节策略
- Parallel Scavenge收集器还有一个参数 -XX:+UseAdaptiveSizePolicy值得我们关注。
- 这是一个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics) 。
- 如果对于收集器运作不太了解,手工优化存在困难的话,使用Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成也许是一个很不错的选择。只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用-XX:MaxGCPauseMillis参数(更关注最大停顿时间)或-XX:GCTimeRatio(更关注吞吐量)参数给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。
Full GC
Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
使用场景
- 这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。
- 如果在服务端模式下,它也可能有两种用途:
- 一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old 收集器以外别无选择,其他表现良好的老年代收集器,如CMS无法与它配合工作。(实际上是 PS MarkSweep GC,但基本与 Serial Old GC 一致,所以就用 Serial Old 代替说明)
- 另外一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并行收集,基于标记-整理算法实现。
- 这个收集器是直到JDK 6时才开始提供的
- 由于 JDK 5 及之前的版本,Parallel Scavenge收集器的老年代Serial Old收集器在服务端应用性能上的“拖累”,所以也未必能在整体上获得吞吐量最大化的效果。
- 同样,由于单线程的老年代收集中无法充分利用服务器多处理器的并行处理能力,在老年代内存空间很大而且硬件规格比较高级的运行环境中,这种组合的总吞吐量甚至不一定比ParNew加CMS的组合来得优秀。
使用场景
- 到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。
老年代
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,是基于标记-清除算法实现的。
步骤
- 初始标记(CMS initial mark)
- 需要“Stop The World”,初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快
- 并发标记(CMS concurrent mark)
- 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
- 重新标记(CMS remark)
- 需要“Stop The World”,采用增量更新,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短
- 并发清除(CMS concurrent sweep)
- 最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
缺点
-
首先,CMS收集器对处理器资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计算能力)而导致应用程序变慢,降低总吞吐量。
- CMS默认启动的回收线程数是(处理器核心数量+3)/4,也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的处理器运算资源,并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时,CMS对用户程序的影响就可能变得很大。如果应用本来的处理器负载就很高,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然大幅降低。
- 为了缓解这种情况,虚拟机提供了一种称为“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS)的CMS收集器变种,所做的事情和以前单核处理器年代PC机操作系统靠抢占式多任务来模拟多核并行多任务的思想一样,是在并发标记、清理的时候让收集器线程、用户线程交替运行,尽量减少垃圾收集线程的独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得较少一些,直观感受是速度变慢的时间更多了,但速度下降幅度就没有那么明显。但实践证明增量式的CMS收集器效果很一般,到JDK 9发布后i-CMS模式被完全废弃。
-
然后,由于CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current ModeFailure”失败进而导致另一次完全“Stop The World”的Full GC的产生。
-
浮动垃圾:在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。
-
预留空间:同样也是由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用
-
后备预案:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。
-
参数 -XX:CMSInitiatingOccupancyFraction 可以设置CMS的触发百分比,设置得太高将会很容易导致大量的并发失败产生,性能反而降低,用户应在生产环境中根据实际应用情况来权衡设置。
-
在JDK5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在实际应用中老年代增长并不是太快,可以适当调高降低内存回收频率,获取更好的性能
-
到了JDK 6时,CMS收集器的启动阈值就已经默认提升至92%。
-
-
-
还有最后一个缺点,CMS是一款基于“标记-清除”算法实现的收集器,意味着收集结束时会有大量空间碎片产生,空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。
- 为了解决这个问题,CMS收集器提供了一个-XX:+UseCMS-CompactAtFullCollection开关参数(默认是开启的,此参数从JDK 9开始废弃),用于在CMS收集器进行Full GC时进行内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,(在Shenandoah和ZGC出现前)是无法并发的。
- 这样空间碎片问题是解决了,但停顿时间又会变长,因此虚拟机设计者们还提供了另外一个参数-XX:CMSFullGCsBefore-Compaction(此参数从JDK 9开始废弃),这个参数的作用是要求CMS收集器在执行过若干次(数量由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理(默认值为0,表示每次进入Full GC时都进行碎片整理)。