jvm系列二之GC收集器

参考

发现有个作者整理的已经很详细了,笔者仅做一些简单的修改记录在这里,方便查阅自己的理解。原文《java垃圾收集器》基本是对《深入理解java虚拟机》一书的总结。另外笔者还参考了Java Garbage Collection Basics

概念理解

并发和并行

这两个名词都是并发编程中的概念,在谈论垃圾收集器的上下文语境中,它们可以解释如下。
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

虽然原书是这么讲的,总感觉不对,所以查阅了文档,私以为只是作者没有描述清楚

吞吐量

吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。
虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

GC垃圾收集器

jvm系列二之GC收集器

图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。

Serial New收集器

特性

  • 最基本的收集器
  • 针对新生代的收集器,采用的是复制算法
  • 单线程
  • 能与CMS收集器配合工作

优势
简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

应用场景
Serial收集器是虚拟机运行在Client模式下的默认新生代收集器。

Parallel New(并行)收集器

特性

  • 使用多线程进行垃圾收集(Serial的多线程版本)
  • 新生代采用复制算法,老年代采用标记整理
  • 能与CMS收集器配合工作
  • -XX:UseConcMarkSweepGC的默认新生代收集器
  • 可以通过-XX:UseParNewGC选项强制指定
  • -XX:ParallelGCThreads参数可以限制垃圾收集线程数

应用场景
ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。
主要原因是:除了Serial收集器外,目前只有它能与CMS收集器配合工作。

Parallel Scavenge(并行)收集器

  • 与ParNew类似的是,都是用于年轻代回收的使用复制算法的并行收集器
  • 该收集器的目标是达到一个可控制的吞吐量,其中吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
  • Parallel Scavenge提供了两个参数用以精确控制吞吐量,分别是用以控制最大GC停顿时间的-XX:MaxGCPauseMillis及直接控制吞吐量的参数-XX:GCTimeRatio.
  • MaxGCPauseMiilis:单位为ms,适用于高用户体验的场景,虚拟机将尽力保证每次MinorGC耗时不超过所设时长,但并不是该时间越小越好,因为GC耗时缩短是用调小年轻代获取的,回收500m的对象肯定要比回收2000m的对象耗时更短,但是回收频率也大大增大了,吞吐量也随之下去了。使用该参数的理论效果:MaxGCPauseMillis越小,单次MinorGC的时间越短,MinorGC次数增多,吞吐量降低。
  • GCTimeRatio:从字面意思上理解是花费在GC上的时间占比,但是实际含义并非如此,GC耗时的计算公式为1/(1+n),n为GCTimeRatio,因此,GCTimeRatio的实际用途是直接指定吞吐量。GCTimeRatio的默认值为99,因此,GC耗时的占比应为1/(1+99)=1%。使用参数的理论效果:GCTimeRatio越大,吞吐量越大,GC的总耗时越小。有可能导致单次MinorGC耗时变长。适用于高运算场景。
  • 此外,还有个参数和以上两个参数息息相关,那就是-XX:+UseAdaptiveSizePolicy,默认为启用,搭配MaxGCPauseMillis或GCTimeRatio使用,打开该开关后,虚拟机将根据当前系统运行情况收集性能监控信息,动态调整SurvivorRatio,PretenureSizeThreshold等细节参数。

使用场景
该收集器是server模式下的默认收集器,也可-XX:+UseParallelGC强制使用该收集器,打开该收集器后,将使用Parallel Scavenge(年轻代)+Serial Old(老年代)的组合进行GC。

Serial Old(串行)收集器

特性
Serial New收集器的老年代版本,使用用标记整理算法

使用场景

主要意义是提供给client模式下的虚拟机用。
虚拟机在Server模式下有两大用途:

  1. JDK<=1.5的虚拟机中与Parallel Scavenge(并行)收集器搭配使用
  2. 作为CMS收集器的后备方案

Parallel Old(并行)收集器

特性

  • Parallel Scavenge收集器的老年代版本
  • 针对老年代,多线程,标记整理算法

使用场景
在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

这个收集器是在JDK 1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old收集器外别无选择(Parallel Scavenge收集器无法与CMS收集器配合工作)。由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合“给力”。直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合。

CMS收集器

特性
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。

运作过程

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

初始标记(CMS initial mark)

初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。

并发标记(CMS concurrent mark)

并发标记阶段就是进行GC Roots Tracing的过程。

重新标记(CMS remark)

重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,仍然需要“Stop The World”。

并发清除(CMS concurrent sweep)

并发清除阶段会清除对象。

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

优点
CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿。

缺点

1.CMS收集器对CPU资源非常敏感
其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。

  1. CMS默认启动的回收线程数是(CPU数量+3)/ 4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个(譬如2个)时,CMS对用户程序的影响就可能变得很大。
  2. CMS收集器无法处理浮动垃圾
    CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
  3. CMS收集器会产生大量空间碎片
    CMS是一款基于“标记—清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。

G1收集器(JDK)

Getting Started with the G1 Garbage Collector

堆内存分配

G1的堆内存被分成许多固定大小的区域。区域大小相同,在JVM启动时确定。JVM通常管理2000个区域,区域大小介于1M到32M之间。
这些区域被映射为逻辑上的Eden、survivor和老年代区域,相同类型区域地址可以不连续。
jvm系列二之GC收集器

区域分为五种,Eden、survivor和老年代,另外还有一种大对象区(Humongous),Humongous区域用来存放占据50%以上区域空间的对象,这些对象被存储在一组连续区域内,最后一种区域就是未被使用区域。

G1的Young GC

存活对象移动到一个或多个survivor区域。如果对象达到晋升年龄,将被移动到老年代区域。这一阶段会Stop The World(STW)。

G1老年代垃圾回收

G1回收器在对内存的老年代区域执行以下阶段。注意这些阶段也包括新生代的回收。

阶段 描述
初始标记(STW) 捎带一次youngGC。标记可能引用了老年代区域对象的survivor区域(根区域)
根区域扫描 扫描根区域中指向老年代的引用。与应用线程并发。此阶段完成后才可以进行youngGC
并发标记 全堆扫描存活对象。与应用线程并发。这一阶段可以被youngGC打断
重新标记STW 完成堆中所有存活对象的扫描,使用snapshot-at-the-beginningSATB算法
清除 统计存活对象和空区域(STW),更新RSets(STW),重置空区域,加入空白列表(并发)
复制STW 将存活对象移动至未使用区域