JVM06-经典垃圾收集器

文章目录

    • 前言
    • 相关概念
      • 并行和并发
      • 吞吐量(Throughput)
      • Minor GC和Full GC
    • 新生代收集器
      • Serial收集器
      • ParNew收集器
      • Parallel Scavenge收集器
    • 老年代收集器
      • Serial Old收集器
      • Parallel Old收集器
      • CMS收集器
        • 优点
        • 缺点
    • G1收集器
      • 横跨整个堆内存
      • 建立可预测的时间模型
      • 避免全堆扫描-Remembered Set
    • 总结
    • 参考资料

前言

上一篇我们介绍了JVM中几种常见的垃圾收集算法。这一篇介绍下七种经典的垃圾收集器,如下图所示:
JVM06-经典垃圾收集器_第1张图片
上图展示了7种作用于不同分代的垃圾收集器。如果两个收集器之间存在连线,则说明它们可以搭配使用。图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器。后面会对这7种垃圾收集器做详细介绍,主要是从这些收集器的目标、特性、原理和使用场景进行介绍。

相关概念

并行和并发

  1. 并行(Parallel): 指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  2. 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行。而垃圾收集程序运行在另一个CPU上。

吞吐量(Throughput)

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

Minor GC和Full GC

  1. 新生代GC(Minor GC): 指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
    触发条件:当Eden区的空间耗尽了怎么办?这时候Java虚拟机便会触发一次Minor GC来手机新生代的垃圾,存活下来的对象,则会被送到Survivor区。
    Minor GC的过程:
    新生代共有两个Survivor区,我们分别用from和to来指代,其中to指向的Survivor区是空的。
    当发生Minor GC时,Eden区和from指向的Survivor区中存活的对象会被复制到to指向的Survivor区中,然后交换from和to指针,以保证下一次Minor GC时,to指向的Survivor区还是空的。
  2. 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随着至少一次的Minor GC(但非绝对的,在Parallel Scavenge 收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般都会被Minor GC慢10倍以上。
  3. Full GC: 收集整个堆,包括新生代、老年代、永久代(在JDK1.8及之后,永久代被移除,换为metaspace元空间)等所有部分的模式。触发的条件是:当准备要触发一次 Minor GC时,如果发现统计数据说之前的Minor GC的平均晋升大小要比目前的老年代的剩余空间大,则会直接触发Full GC

新生代收集器

Serial收集器

Serial(串行)收集器是JDK1.3之前是虚拟机新生代收集的唯一选择。。它是一个单线程工作的收集器,并且其进行垃圾收集时,用户线程需要暂停,直到垃圾收集结束为止(“Stop The World”)。这就好像你老妈打扫房间时,你需要离开房间一样的道理。其采用的收集算法是标记-复制算法,如下图就是Serial/Serial Old收集器搭配使用的运行示意图。
JVM06-经典垃圾收集器_第2张图片
Serial收集器的优点就是简单高效,额外内存消耗最小。对于单核处理器或者处理器核数较少的环境来说,Serial收集器没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。所以,它依然是HotSpot虚拟机运行在Client模式下的默认的新生代收集器。

ParNew收集器

ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio)、收集算法、暂停用户线程、对象分配规则、回收策略等都与Serial收集器完全一致。同样的其采用的收集算法是标记-复制算法。下图就是ParNew/Serial Old收集器搭配使用的运行示意图。
JVM06-经典垃圾收集器_第3张图片
ParNew收集器除了使用多线程收集外,其他方面与Serial收集器相比并无太多创新之处,但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中一个重要的原因是,除了Serial收集器之外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作, CMS收集器后面会详细介绍。
ParNew收集器在单CPU的环境中绝对不会有比Serial收集器有更好的效果,在多CPU环境下,随着CPU的数量增加,它对于GC时系统资源的有效利用是很有好处的,它默认开启的收集线程数与CPU的数量相同,可以通过-XX:ParallerGCThreads参数设置。

Parallel Scavenge收集器

Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器,Parallel Scavenge的诸多特性从表面上看与ParNew非常相似,那它有什么特别之处呢?
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程停顿的时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。吞吐量的说明前面有提到。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大 垃圾收集停顿时间的 -XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 -XX:GCTimeRatio 参数。
-XX:MaxGCPauseMillis 参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定值。
-XX:GCTimeRatio参数的值则应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。
由于与吞吐量关系密切,Parallel Scavenge收集器也经常被称作"吞吐量优先收集器"。

老年代收集器

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。如果在服务器模式下,它也可能有两种用途, 一种是在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另外一种就是作为CMS收集器发生失败时的后备预案。下图就是Serial/Serial Old收集器搭配使用的运行示意图。
JVM06-经典垃圾收集器_第4张图片

Parallel Old收集器

Parallel Old收集器是Parallel Scavenage收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。下图就是Parallel Scavenge/Parallel Old收集器运行示意图。
JVM06-经典垃圾收集器_第5张图片

CMS收集器

CMS(Concurrent Mark Sweep)收集器是JDK1.5推出的一个具有划时代意义的收集器,是一种以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。从名字上看它是基于"标记-清除"算法实现的。
CMS收集器工作的这个流程分为4个步骤:

  1. 初始标记(CMS initial mark) : 仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要"Stop The World"。
  2. 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程,在整个过程耗时最长。
  3. 重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,此阶段也需要"Stop The World"。
  4. 并发清除(CMS concurrent sweep) :清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活的对象,所以这个阶段也是可以与用户线程同时并发的。
    由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程就是与用户线程一起并发执行的。通过下图,可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的阶段。
    JVM06-经典垃圾收集器_第6张图片

优点

CMS是一款优秀的收集器,它最主要的优点就是:并发收集、低停顿,因此CMS收集器也被称为并发低停顿收集器(Concurrent Low Pause Collector)

缺点

  1. 对CPU资源非常敏感,在并发阶段,它虽然不会导致应用程序变慢,但是总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降,但是当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大, 如果本来CPU负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低50%,其实也让人无法接受。
  2. 无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败而导致另一次Full GC的产生,由于CMS并发清理阶段用户线程还在运行着,伴随着程序运行自然就会有新的垃圾不断产生。 这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就被称为“浮动垃圾”。
  3. 标记-清除算法导致的空间碎片 CMS收集器是基于"标记-清除"算法实现的收集器,这意味着每次收集结束之后会有大量空间碎片产生。

G1收集器

G1(Garbage-First)收集器是当今收集器技术发展最前沿的成果之一,他是一款面向服务端应用的垃圾收集器,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点:

  1. 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短"Stop The World"停顿时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  2. 分代收集与其他收集器一样,分代概念在G1中仍然得以保留,虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。
  3. 空间整合 G1从整体来看是基于"标记-整理"算法实现的收集器,从局部(两个Region之间)上来看是基于"复制"算法实现的。这意味着G1运作期间不会产生内存碎片,垃圾收集完成之后能够提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  4. 可预测的停顿:这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型。能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎是实时Java(RTSJ)的垃圾收集器的特征了。

横跨整个堆内存

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

建立可预测的时间模型

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

避免全堆扫描-Remembered Set

G1把Java堆分为多个Region,就是"化整为零"。但是Region不可能是孤立的,一个对象分配在某个Region中,可以与整个Java堆任意的对象发生引用关系。在做可达性分析缺点对象是否存活的时候,需要扫描整个Java堆才能保证准确性,这显然是对GC效率的极大伤害。
为了避免全堆扫描的发生。虚拟机为G1中每个Region维护了一个与之对应的RememberedSet。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围内加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤 :

  1. 初始标记(Initial Marking) 仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程,但耗时很短。
  2. 并发标记(Concurrent Marking) 从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行
  3. 最终标记(Final Marking) 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs 里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中, 这阶段需要停顿线程,但是可并行执行。
  4. 筛选回收(Live Data Counting and Evacuation) 首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分的Region的存活对象复制到空的Region中。再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
    下图比较清楚的展示了G1收集器的运作步骤。
    JVM06-经典垃圾收集器_第7张图片

总结

本文主要介绍了JVM七种经典的收集器做完了详细的说明。之所以要有这么多收集器,是因为没有一种收集器是完美的。在JDK1.5 推出来具有划时代意义的CMS收集器,它的特点就是并发收集,低停顿。而JDK1.7推出了收集器的集大成者—G1收集器。它的特点就是并发收集,可预测的停顿,不会产生碎片化

收集器 串行、并行or并发 新生代/老年代 算法 目标 适用场景
Serial 串行 新生代 标记-复制算法 响应速度优先 单CPU环境下Client模式
ParNew 并行 新生代 标记-复制算法 响应速度优先 多CPU环境时在Server模式下与CMS配合
Parallel Scavenge 并行 新生代 标记-复制算法 吞吐量优先 在后台运算不需要太多交互的任务
Serial Old 串行 老年代 标记-整理算法 响应速度优先 单CPU环境下的Client模式、CMS的后备预案
Parallel Old 并行 老年代 标记-整理算法 吞吐量优先 在后台运算而不需要太多交互的任务
CMS 并行 老年代 标记-清除 响应速度优先 集中在互联网站或B/S系统服务端上的Java应用
G1 并行 所有 标记-整理+标记-复制算法 响应速度优先 面向服务端应用,将来替换CMS

参考资料

深入理解JVM(3)——7种垃圾收集器
深入理解Java虚拟机(第3版)

你可能感兴趣的:(JVM)