GC 垃圾收集器
1. 垃圾收集器概览
1.1 概述
如果说 JVM 的垃圾回收算法是内存回收的抽象策略,那么垃圾收集器就是内存回收的具体实现。
JVM 规范对于垃圾收集器应该如何实现没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器差别较大,这里只看 HotSpot 虚拟机。
就像没有最好的算法一样,垃圾收集器也没有最好的,只有最合适的。我们能做的就是根据具体的应用场景选择最合适的垃圾收集器。
1.2 针对新生代
-Serial
-ParNew
-Parallel Scavenge
1.3 针对老年代
-Serial Old
-Parallel Old
-CMS
1.4 最新万能版
-G1
-收集器参数总结
2. 垃圾收集器详解
2.1 Serial 收集器
Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了(新生代采用复制算法,老年代采用标志整理算法)。看名字就可以知道这个收集器是一个单线程的收集器。
它的 "单线程" 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程(Stop The World:将用户正常工作的线程全部暂停掉),直到它收集结束。

上图中:
1、新生代采用复制算法,Stop-The-World
2、老年代采用标记-整理算法,Stop-The-World
当它进行 GC 工作的时候,虽然会造成 Stop-The-World,正如每种算法都有存在的原因,该串行收集器也有存在的原因:因为简单而高效(与其他收集器的单线程相比),对于限定单个 CPU 的环境来说,没有线程交互的开销,专心做 GC,自然可以获得最高额单线程效率。所以 Serial 收集器对于运行在 Client 模式下应用是一个很好的选择(到目前为止,它依然是虚拟机运行在 Client 模式下默认的新生代收集器)
串行收集器的缺点很明显,虚拟机的开发者当然也是知道这个缺点的,所以一直都在缩减 Stop The World 的时间。
在后续的垃圾收集器设计中停顿时间不断缩短(但是仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。
整理一定下前面关于 Serial 收集器的特点:
1、针对新生代的收集器;
2、采用复制算法;
3、单线程收集;
4、进行垃圾收集时,必须暂停所有工作线程,直到完成,即会 "Stop The World";
应用场景:
1、依然是 HotSpot 在 Client 模式下默认的新生代收集器;
2、也有优于其他收集器的地方:
-简单高效(与其他收集器的单线程相比);
3、对于限定单个 CPU 的环境来说,Serial 收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;
4、在用户的桌面应用场景中,可用的内存一般不大(几十 M 至一两百 M),可以在较短时间内完成垃圾收集(几十 MS 至一百多 MS),只要不频繁发生,这是可以接受的;
设置参数:
1、添加该参数来显示使用的串行垃圾收集器
-XX: + UseSerialGC
2.2 ParNew 收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、手机算法、回收策略等等)和 Serial 收集器完全一样。
它是许多在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。
CMS 收集器是一个被认为具有划时代意义的并发收集器,因此如果有一个垃圾收集器能和它一起搭配使用让其更加完美,那这么收集器必然是一个不可或缺的部分了。

特点:
1、除了多线程外,其余的行为、特点和 Serial 收集器一样;
2、如 Serial 收集器可控制参数、收集算法、Stop The World、内存分配、回收策略等;
3、Serial 收集器共用了不少代码;
应用场景:
1、在 Server 场景下,ParNew 收集器是一个非常重要的收集器,因此除 Serial 外,目前只有它能与 CMS 收集器配合工作;
2、在单个 CPU 环境中,不会比 Serial 收集器有更好的效果,因此存在线程交互开销;
设置参数:
1、指定使用 CMS 后,会默认使用 ParNew 作为新生代收集:
-XX: + UseConcMarkSweepGC
2、强制使用 ParNew:
-XX:+UseParNewGC
3、指定垃圾收集器的线程数量,ParNew 默认开启的收集线程与 CPU 的数量相等:
-XX:ParallelGCThreads
2.2.1 为什么只有 ParNew 能与 CMS 收集器配合
1、CMS 是 HotSpot 在 JDK 1.5 推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;
2、CMS 作为老年代收集器,但却无法与 JDK 1.4 已经存在的新生代收集器 Parallel Scavenge 配合工作;
3、因为 Parallel Scavenge(以及 G1)都没有使用传统的 GC 收集器代码框架,而另外独立实现。而其余几种收集器则共用了部分的框架代码。
2.3 Parallel Scavenge 收集器
Parallel Scavenge 收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
Parallel Scavenge 收集器关注点是吞吐量(如何高效率利用 CPU)。
CMS 等垃圾收集器的关注点更多的是在用户线程的停顿时间(提高用户体验)。
所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。(吞吐量:CPU 用于用户代码的时间 / CPU 总消耗时间的比值,即 = 运行用户代码的时间 / (运行用户代码时间 + 垃圾收集时间)。比如,虚拟机总共运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99%)

Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,不进行手工优化,可以选择把内存管理优化交给虚拟机去完成。
特点:
1、新生代收集器;
2、采用复制算法;
3、多线程收集;
4、CMS 等收集器的关注点是尽可能的缩短垃圾收集时用户线程的停顿时间;而 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(ThroughPut);
应用场景:
1、高吞吐量为目标,既减少垃圾收集的时间,让用户代码获得更长的运行时间;
2、当应用程序运行在具有多个 CPU 上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需与用户进行太多交互;
3、例如,那些执行批量处理、订单处理(对账等)、工资支付、科学计算的应用程序。
设置参数:
Parallel Scavenge 收集器提供两个参数用于精确控制吞吐量:
1、控制最大垃圾收集停顿的时间:
-XX:MaxGCPauseMillis
a.控制最大垃圾收集停顿时间,大于 0 的毫秒数
b.MaxGCPauseMillis 设置的稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降;因为可能导致垃圾收集发生的更频繁。
2、设置垃圾收集时间占总时间的比率:
-XX:GCTimeRatio
设置垃圾收集时间占总时间的比率,0 < n < 100 的整数;
GCTimeRatio 相当于设置吞吐量大小;
垃圾收集执行时间占应用程序执行时间的比例计算方法是:1 / (1 + n)。例如,选项 -XX:GCTimeRatio = 19,设置了垃圾收集时间占总时间的 5% = 1 / (1 + 19);默认值是 1% = 1 / (1 + 99),即 n = 99;
垃圾收集所花费的时间是年轻代和老年代收集的总时间;
2.4 GC 自适应的调节策略(GC Ergonomics)
另外还有一个参数:
-XX:+UseAdptiveSizePolicy
开启这个参数后,就不用手工指定一些细节参数,比如:
1、新生代的大小(-Xmm)、Eden 与 Survivor 区的比列(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等;
2、JVM 会根据当前系统运行的情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为 GC 自适应的调节策略(GC Ergonomics);
3、这是一种值得推荐的方式:
a.只需设置好内存数据大小(如"-Xmx"设置最大堆);
b.然后使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"给 JVM 设置一个优化目标;
c.那些具体细节参数的调节就由 JVM 自适应完成。
这也是 Parallel Scavenge 收集器与 ParNew 收集器一个重要的区别。
2.5 Serial Old 收集器
Serial 收集器的老年代版本,它同样是一个单线程收集器。
它主要有两大用途:一种用途是在 JDK 1.5 及以前的版本中与 Parallel Scavenge 收集器搭配使用;另一种用途是作为 CMS 收集器的后备方案。
特点:
1、针对老年代;
2、采用"标记-整理-压缩"算法(Mark-Sweep-Compact);
3、单线程收集。
Serial/Serial Old收集器运行示意图在前面有。
应用场景:
1、主要用于 Client 模式;
2、而在 Server 模式有两大用途:
a.在 JDK 1.5 及以前,与 Parallel Scavenge 收集器搭配使用(JDK 1.6 有 Parallel Old 收集器可搭配 Parallel Scavenge 收集器);
b.作为 CMS 收集器的后备预案,在并发收集器发生 Concurrent Mode Failure 时使用。
2.6 Parallel Old 收集器
Parallel Scavenge 收集器的老年代版本。
使用多线程和"标记-整整"算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
在 JDK 1.6 才有的。
特点:
1、针对老年代;
2、采用"标记-整理-压缩"算法;
3、多线程收集。
Parallel Scavenge/Parallel Old 收集器运行示意图如下:

应用场景:
1、JDK 1.6 及以后用来代替老年代的 Serial Old 收集器;
2、特别是在 Server 模式,多 CPU 情况下:
a.这样在注重吞吐量以及 CPU 资源敏感的场景,就有了 Parallel Scavenge(新生代)加 Parallel Old(老年代)收集器的"给力"应用组合。
设置参数:
1、指定使用 Parallel Old 收集器:
-XX: + UseParallelOldGC
2.7 CMS (Concurrent Mark Sweep)收集器
CMS(Concurrent Mark Sweep)收集器是一种以获得最短回收停顿时间为目标的收集器。它非常适合在注重用户体验的应用上使用。
特点:
1、针对老年代;
2、基于"标记-清除"算法(不进行压缩操作,会产生内存碎片);
3、以获取最短回收停顿时间为目标;
4、并发收集、低停顿;
5、需要更多的内存。
CMS 是 HotSpot 在 JDK 1.5 推出的第一款真正意义上的并发(Concurrent)收集器;第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
应用场景:
1、与用户交互较多的场景(如常见 WEB、B/S-浏览器/服务器模式系统的服务器上应用);
2、希望系统停顿时间最短,注重服务响应速度:
a.以给用户带来较好的体验;
设置参数:
1、指定使用 CMS 收集器
-XX:+UseConcMarkSweepGC
2.7.1 CMS 收集器运作过程
从名字中的 Mark Sweep 这两个词可以看出,CMS 收集器是一种"标记-清除"算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
1、初始标记:
暂停所有的其他线程,初始标记仅仅标记 GC Roots 能直接关联到的对象,速度很快;
2、并发标记:
a.并发标记就是进行 GC Roots Tracing 的过程
b.同时开启 GC 和用户线程,用一个闭包结构去纪录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方;
3、重新标记:
重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(采用多线程并行执行来提升效率);需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;
4、并发清除:
开启用户线程,同时 GC 线程开始对标记的区域做清扫,回收所有的垃圾对象。
由于整个过程耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作。
所以总体来说,CMS 的内存回收是与用户线程一起"并发"执行的。
CMS 收集器运行示意图如下:

2.7.2 CMS 收集器缺点
2.7.2.1 对 CPU 资源敏感
面向并发设计的程序都对 CPU 资源比较敏感(并发程序的特点)。在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说 CPU 资源)而导致应用程序变慢,总吞吐量会降低。(在对账系统中,不适合使用 CMS 收集器)。
CMS 的默认收集线程数量是 = (CPU 数量 + 3) / 4;当 CPU 数量越多,回收的线程占用 CPU 就少。
也就是当 CPU 在 4 个以上时,并发回收时垃圾收集线程不少于 25% 的 CPU 资源,对用户程序影响可能较大;不足 4 个时,影响更大,可能无法接受。(比如 CPU = 2 时,那么就启动一个线程回收,占了 50% 的 CPU 资源。)
一个回收线程会在回收期间一直占用 CPU 资源;
针对这种情况,曾出现了"增量式并发收集器"(Incremental Concurrent Mark Sweep/i-CMS);
类似使用抢占式来模拟多任务机制的思想,让收集线程和用户线程交替运行,减少收集线程运行时间;
但效果并不理想,JDK 1.6 后就官方不再提倡用户使用。
2.7.2.2 无法处理浮动垃圾
无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败,在并发清除时,用户线程新产生的垃圾,称为浮动垃圾;
解决办法:
这使得并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集;也可以认为CMS所需要的空间比其他垃圾收集器大;可以使用"-XX:CMSInitiatingOccupancyFraction",设置CMS预留老年代内存空间。
2.7.2.3 产生大量内存碎片
由于 CMS 是基于"标记-清除"算法来回收老年代对象的,因此长时间运行后会产生大量的空间碎片问题,可能导致新生代对象晋升到老生代失败。
由于碎片过多,将会给大对象的分配带来麻烦。因此会出现这样的情况,老年代还有很多剩余的空间,但是找不到连续的空间来分配当前对象,这样不得不提前触发一次 Full GC。
解决办法:
使用"-XX:+UseCMSCompactAtFullCollection"和"-XX:+CMSFullGCsBeforeCompaction",需要结合使用。
2.7.2.4 CMSFullGCsBeforeCompaction
由于合并整理是无法并发执行的,空间碎片问题没有了,但是有导致了连续的停顿。因此,可以使用另一个参数−XX:CMSFullGCsBeforeCompaction,表示在多少次不压缩的 Full GC 之后,对空间碎片进行压缩整理。
可以减少合并整理过程的停顿时间;
默认为 0,也就是说每次都执行 Full GC,不会进行压缩整理;
由于空间不再连续,CMS 需要使用可用"空闲列表"内存分配方式,这比简单实用"碰撞指针"分配内存消耗大。
2.8 G1 收集器

上一代的垃圾收集器(串行 serial, 并行 parallel, 以及 CMS)都把堆内存划分为固定大小的三个部分:
-年轻代(young generation)
-年老代(old generation)
-持久代(permanent generation)
注:堆内存中都可以认为是 Java 对象
G1 (Garbage-First)是 JDK 7-u4 才推出商用的收集器;
G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的及其。以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。被视为 JDK 1.7 中 HotSpot 虚拟机的一个重要进化特征。
G1 的使命是在未来替换 CMS,并且在 JDK 1.9 已经成为默认的收集器。
2.8.1 G1 收集器特点
2.8.1.1 并行与并发
G1 能够充分利用 CPU 多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 Java 程序继续执行。
2.8.1.2 分代收集
虽然 G1 可以不需要其它收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
a.能独立管理整个 GC 堆(新生代和老年代),而不需要与其他收集器搭配;
b.能够采用不同方式处理不同时间的对象;
c.虽然保留分代概念,但 Java 堆的内存布局有很大差别;
d.将整个堆划分为多个大小相等的独立区域(Region);
e.新生代和老年代不再是物理隔离,他们都是一部分 Region(不需要连续)的集合。
2.8.1.3 空间整理
与 CMS 的"标记-清除"算法不同,G1 从整体来看是基于"标记-整理"算法实现的收集器;从局部上来看就是基于"复制"算法实现的。
a.从整体上看,是基于标记-整理算法
b.从局部(两个 Region 间)看,是基于复制算法
1、这是一种类似火车算法的实现;
2、不会产生内存碎片,有利于长时间运行。
2.8.1.3 可预测停顿
这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型。可以明确指定 M 毫秒时间片内,垃圾收集消耗的时间不超过 N 毫秒。在低停顿的同时实现高吞吐量。
2.8.2 关于 G1 的问题
2.8.2.1 为什么G1可以实现可预测停顿
1、可以有计划地避免在 Java 堆的进行全区域的垃圾收集;
2、G1 收集器将内存分大小相等的独立区域(Region),新生代和老年代概念保留,但是已经不再物理隔离;
3、G1 跟踪各个 Region 获得其收集价值大小,在后台维护一个优先列表;
4、每次根据允许的收集时间,优先回收价值最大的 Region(名称Garbage-First的由来);
这就保证了在有限的时间内可以获取尽可能高的收集效率。
2.8.2.2 一个对象被不同区域引用的问题
一个 Region 不可能是孤立的,一个 Region 中的对象可能被其他任意 Region 中对象引用,判断对象存活时,是否需要扫描整个 Java 堆才能保证准确?
在其他的分代收集器,也存在这样的问题(而G1更突出):回收新生代也不得不同时扫描老年代?
这样的话会降低 Minor GC 的效率;
解决办法:
无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描;
每个Region都有一个对应的Remembered Set;
每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作;
然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象);
如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;
当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set;
就可以保证不进行全局扫描,也不会有遗漏。
2.8.2.3 应用场景
1、面向服务端应用,针对具有大内存、多处理器的机器;
2、最主要的应用是为需要低 GC 延迟,并具有大堆的应用程序提供解决方案
如:在堆大小约 6GB 或更大时,可预测的暂停时间可以低于 0.5 秒;
(实践:对账系统中将 CMS 垃圾收集器修改为 G1,降低对账时间 20 秒以上)
具体什么情况下应用 G1 垃圾收集器比 CMS 好,可以参考以下几点(但不是绝对):
1、超过 50% 的 Java 堆被活动数据占用;
2、对象分配频率或年代的提升频率变化很大;
3、GC 停顿时间过长(长于 0.5 至 1 秒);
建议:
1、如果现在采用的收集器没有出现问题,不用急着去选择 G1;
2、如果应用程序追求低停顿,可以尝试选择 G1;
3、是否代替 CMS 只有需要实际场景测试才知道。(如果使用 G1 后发现性能还没有使用 CMS 好,那么还是选择 CMS 比较好)
2.8.2.4 设置参数
可以通过下面的参数,来设置一些 G1 相关的配置。
1、指定使用 G1 收集器:
-XX:+UseG1GC
2、当整个 Java 堆的占用率达到参数值时,开始并发标记阶段;默认为 45:
-XX:InitiatingHeapOccupancyPercent
3、为G1设置暂停时间目标,默认值为 200 毫秒:
-XX:MaxGCPauseMillis
4、设置每个 Region 大小,范围 1MB 到 32MB;目标是在最小 Java 堆时可以拥有约 2048 个 Region:
-XX:G1HeapRegionSize
5、新生代最小值,默认值 5%:
-XX:G1NewSizePercent
6、新生代最大值,默认值 60%:
-XX:G1MaxNewSizePercent
7、设置 STW 期间,并行 GC 线程数:
-XX:ParallelGCThreads
8、设置并发标记阶段,并行执行的线程数:
-XX:ConcGCThreads
2.8.3 G1 运作过程
不计算维护 Remembered Set 的操作,可以分为 4 个步骤(与 CMS 较为相似)。
1、初始标记(Initial Marking)
仅标记一下 GC Roots 能直接关联到的对象;
且修改 TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的 Region 中创建新对象;
需要"Stop The World",但速度很快;
2、并发标记(Concurrent Marking)
从 GC Roots 开始进行可达性分析,找出存活对象,耗时长,可与用户线程并发执行,但是并不能保证可以标记出所有的存活对象;(在分析过程中会产生新的存活对象)。
3、最终标记(Final Marking)
修正并发标记阶段因用户线程继续运行而导致标记发生变化的那部分对象的标记记录。
上一阶段对象的变化记录在线程的 Remembered Set Log;
这里把 Remembered Set Log 合并到 Remembered Set 中;
需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;
G1 采用多线程并行执行来提升效率;且采用了比 CMS 更快的初始快照算法:Snapshot-At-The-Beginning (SATB)。
4、筛选回收(Live Data Counting and Evacuation)
首先排序各个 Region 的回收价值和成本;
然后根据用户期望的 GC 停顿时间来制定回收计划;
最后按计划回收一些价值高的 Region 中垃圾对象;
回收时采用"复制"算法,从一个或多个 Region 复制存活对象到堆上的另一个空的 Region,并且在此过程中压缩和释放内存;
可以并发进行,降低停顿时间,并增加吞吐量。
2.8.4 总结
G1 在标记过程中,每个区域的对象活性都被计算,在回收时候,就可以根据用户设置的停顿时间,选择活性较低的区域收集,这样既能保证垃圾回收,又能保证停顿时间,而且也不会降低太多的吞吐量。Remark(重新标记)阶段新算法的运用,以及收集过程中的压缩,都弥补了 CMS 不足。
引用 Oracle 官网的一句话:"G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS)";
G1 计划作为并发标记-清除收集器(CMS)的长期替代品。