JVM:GC之垃圾收集器

简述

如果说收集算法时内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。这里我们讨论的垃圾收集器是基于JKD1.7之后的Hotspot虚拟机,这个虚拟机包含的所有收集器如图:
JVM:GC之垃圾收集器_第1张图片

Serial 收集器

Serial收集器是一个单线程收集器,它在进行垃圾收集时,(Stop The World)必须暂停其它所有的工作线程,直到它收集结束。这对很多应用来说是无法接受的,试想你的计算机每运行1小时就会暂停5分钟,是否有种想砸电脑的冲动。

Serial 收集器运行示意图
JVM:GC之垃圾收集器_第2张图片
Serial 收集器适用场景

简单高效(与其它收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集可以获得更高的单线程收集效率。因此Serial收集器适用于在client模式下的默认新生代收集器。

ParNew 收集器

ParNew 收集器是 Serial 收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial 收集器可用的所有控制参数、收集算法、Stop The World 、对象分配规则、回收策略等都与Serial收集器完全一样。

ParNew 收集器设置参数:

-XX:+UseConcMarkSweepGC 默认新生代收集器

-XX:+UseParNewGC 强制指定为新生代收集器

-XX:ParallelGCThreads 限制垃圾收集线程数(默认与CPU数量一致)

ParNew 收集器运行示意图
JVM:GC之垃圾收集器_第3张图片

Parallel Scavenge 收集器

Paralleal Scavenge 收集器是一个新生代收集器,也是使用复制算法,又是并行的多线程收集器,看上去和ParNew一样,但它有自己的特别之处。

Parallel Scavenge 收集器关注点是吞吐量(Throughput),吞吐量=运行用户的代码时间(运行用户代码时间+垃圾收集时间)

停顿时间越短就越适合与用户交互的程序,良好的响应速度能提升用户体验,
高吞吐量可以高效率的使用CPU,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务

Parallel Scavenge 收集器设置参数:

-XX:MaxGCPauseMillis 最大垃圾收集停顿时间(大于0的毫秒数)

-XX:GCTimeRatio 吞吐量大小(大于0小于100的整数)

-XX:UseAdaptiveSizePolicy 开启GC自适应调节策略(GC Ergonomics)

GC自适应调节策略开启之后,就不需要手工指定新生代大小(-Xmn)、Eden 与 Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数提供最适合的停顿时间或最大吞吐量。

自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。

Serial Old 收集器

Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法,它主要也是给Client模式下的虚拟机使用。

在Server下的两大用途:

一是在jdk1.5以及之前与Parallel Scavenge收集器搭配使用

二是作为CMS收集器的后配预案,在并发收集发生 Concurrent Mode Failure 时使用

Serial Old 收集器运行示意图:
JVM:GC之垃圾收集器_第4张图片

Parallel Old 收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法。

Parallel Old 收集器在jdk1.6中才开始提供,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是,如果新生代选择了Parallel Scavenge收集器,老年代除了 Serial Old 收集器外别无选择。由于老年代 单线程 Serial Old 收集器在服务端性能上的不佳,新生代使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大的效果。

Parallel Old 收集器的出现,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量及cpu资源敏感的场合,都可以优先考虑Parallel Scavenge 加Parallel Old收集器。

Parallel Scavenge / Parallel Old 收集器运行示意图:
JVM:GC之垃圾收集器_第5张图片

CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,
目前集中应用在互联网站或B/S系统等比较注重用户体验的应用上。

CMS 收集器是基于“标记-清除”算法实现的,它的运行过程相对于前几种收集器相对复杂一些,整个过程分为4个步骤:

  • 初始标记(CMS initial mark)

  • 并发标记(CMS concurrent mark)

  • 重新标记(CMS remark)

  • 并发清除(CMS concurrent sweep)

初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing 的过程,而重新标记则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记短。

由于整个过程耗时最长的并发标记和并发清除两个阶段,收集器线程都可以与用户线程一起工作,所以,从总体上说,CMS收集器的内存回收过程与用户线程是一起并发执行的。

Concurrent Mark Sweep 收集器运行示意图:
JVM:GC之垃圾收集器_第6张图片
CMS是一款优秀的收集器,它的主要优点是:并发收集、低停顿,但是CMS 还远达不到完美的程度,它有以下3个明显的缺点:

1、CMS 收集器对CPU资源非常敏感

在并发阶段,它虽然不会导致用户线程停顿,但是会占用一部分线程,而导致应用程序变慢,总吞吐量会降低。CMS默认启动的线程数是【(CPU数量+3)/4】,也就是当CPU数量在4个以上时,并发回收垃圾的线程将占用不少于25%的CPU资源,并且随着CPU数量的增加而下降。

但是当CPU数量小于4时(譬如2个),CMS对用户程序的影响就可能变得很大,如果本来CPU负载就很大,还要分出一半的运算能力去执行收集线程,就会导致用户程序的执行速度忽然降低50%。

2、CMS 收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次 Full GC 的产生

由于CMS并发清理阶段,用户线程还在运行,这就避免不了产生新的垃圾,但是这新产生的垃圾出现在标记过程之后,在当前的CMS清理中是无法清理的,只能留到下一次GC是清理,这部分垃圾就称为“浮动垃圾”。

由于垃圾收集阶段,用户线程还要运行,那么就需要预留足够的内存空间给用户线程使用。如果预留的内存无法满足程序需要,就会出现“Concurrent Mode Failure” 失败,这时虚拟机将启动后备预案:临时启用Serial Old 收集器重新进行老年代的垃圾收集,这样停顿时间会变的更长。所以参数【-XX:CMSInitiatingOccupancyFraction】设置得太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。

jdk1.5默认设置为,CMS收集器当老年代使用了68%的空间后就会被激活;jdk1.6中CMS收集器的启动阈值提升为92%。

3、产生大量空间碎片

CMS是一款基于“标记-清除”算法实现的收集器,也就意味着收集结束时会有大量碎片产生。空间碎片过多将无法给大对象分配内存,不得不提前触发一次Full GC。

为了解决这个问题,CMS收集器提供了一个【-XX:+UseCMSCompactAtFullCollection】开关设置(默认是开启的),在CMS顶不住要进行FullGC时开启内存碎片的合并整理过程,但是内存碎片整理过程是无法并发的,就会导致停顿时间不得不变长。还有另外一个参数【-XX:CMSFullGCsBeforeCompaction】,这个参数是用于设置执行多少次不压缩的Full GC后,执行一次带压缩的(默认为0,表示每次进行Full GC 时都进行碎片整理)

G1 收集器

G1 (Garbage-First)收集器是一款面向服务端应用的垃圾收集器。与其它收集器相比,G1具备以下特点:

并行与并发

G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World 停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行

分代收集

与其它收集器一样,分代概念在G1中依然得以保留,虽然G1可以不需要其它收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果

空间整合

与CMS的“标记-清理”算法不同,G1从整体上看是基于“标记-整理”算法实现的收集器,从局部来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC

可预测的停顿

这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型。

在G1之前的其它收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其它收集器有很大的差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是Region集合的一部分。

G1之所以能够建立可预测的停顿时间模型,是因为它可以有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(Garbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

G1收集器的运行过程大致可以分为以下几个步骤:

  • 初始标记(Initial Marking)
  • 并发标记(Concurrent Marking)
  • 最终标记(Final Marking)
  • 筛选回收(Live Data Counting and Evacuation)

初始标记阶段仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短

并发标记阶段是从GC Root 开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。

最终标记阶段则是为了修正在并发标记期间因用户程序继续运行而导致标记产生变动的那一部分标记记录,这就阶段需要停顿线程,但可以并行执行。

筛选回收阶段首先对各个Region区域的回收价值和成本进行排序,然后根据用户所期望的GC停顿时间来制定回收计划
JVM:GC之垃圾收集器_第7张图片

收集器常用组合


参考资料:
《深入理解Java虚拟机之jvm高级特性与实践》

你可能感兴趣的:(【JVM】JVM系列,JVM,虚拟机系列)