简单介绍
什么是垃圾收集器
什么是垃圾收集器?
如果说垃圾收集算法是内存回收的方法论,那么「垃圾收集器」就是内存回收的具体实现或者说是内存回收的执行者。
「垃圾收集器」本身并没有优劣之分,我们需要做的是根据具体场景选择合适的「垃圾收集器」。
于是本篇重点在介绍各种「垃圾收集器」的特点,并且介绍它们适合的应用场景和原因。
安全点(SafePoint)
什么是SafePoint?
程序执行时并非在所有地方都能停顿下来开始GC,只有在到达SafePoint时才能暂停。
SafePoint的选定既不能太少以至于让GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。
基本上是以程序「是否具有让程序长时间执行的特征」为标准进行选定的。
「长时间执行」的最明显特征就是「指令序列复用」(例如方法调用、循环跳转、异常跳转等)。所以具有这些功能的指令才会产生Safepoint。
SafePoint的目的并不是让其他线程停下,而是找到一个稳定的执行状态。在这个执行状态下,JVM的堆栈不会发生变化。这样垃圾回收器便能够“安全”地执行「可达性分析」。只要不离开这个SafePoint,JVM便能够在垃圾回收的同时,继续运行这段本地代码。
如何在 SafePoint 停顿?
对于SafePoint,另一个需要考虑的问题就是如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的SafePoint上再停顿下来。
两种解决方案:
-
抢先式中断(Preemptive Suspension)
「抢先式中断」不需要线程的执行代码主动去配合,在
GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在SafePoint上,就恢复线程,让它“跑”到SafePoint上。现在几乎没有虚拟机采用这种方式来暂停线程从而响应
GC事件。 -
主动式中断(Voluntary Suspension)
「主动式中断」的思想是当
GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志。各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。「轮询标志的地方」和
SafePoint是重合的,另外再加上创建对象需要分配内存的地方。
垃圾收集器
本章会提及几个算法包括但不限于「复制算法」,「标记-整理算法」,「标记清除算法」。
这些内容都在我的博客:从零开始的JVM学习--GC中介绍。
新生代收集器
Serial
什么是Serial?
Serial(串行)是一个单线程的垃圾收集器,只开启一条 GC 线程进行垃圾回收,并且在垃圾收集过程中停止一切用户线程( Stop The World)。
由于 Serial 收集器只使用一条 GC 线程,避免了线程切换的开销,从而简单高效。
Serial在新生代使用「复制算法」。
适用场景
Serial 垃圾收集器适合Client场景下使用。
Serial简单而高效,Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。
一般「客户端」应用所需内存较小,不会创建太多对象,而且堆内存不大,因此垃圾收集器回收时间短,即使在这段时间停止一切用户线程,也不会感觉明显卡顿。
Serial收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。
ParNew
什么是ParNew?
ParNew 是 Serial 的多线程版本。由多条 GC 线程并行地进行垃圾清理。但清理过程依然需要 Stop The World。
ParNew 追求「低停顿时间」,与 Serial 唯一区别就是使用了多线程进行垃圾收集,在多 CPU 环境下性能比 Serial 会有一定程度的提升;但线程切换需要额外的开销,因此在单 CPU 环境中表现不如 Serial。
ParNew在新生代使用「复制算法」。
适用场景
ParNew是 Server 场景下默认的新生代收集器。 (停顿时间越短就越适合与用户交互的程序,良好的相应速度能提升用户体验)
除了Serial收集器,只有ParNew可以和CMS收集器配合使用。
根据上面的介绍我们知道ParNew适合多核CPU环境。并且ParNew 追求低停顿,因此ParNew适合交互式应用。
Parallel Scavenge
什么是Parallel Scavenge?
Parallel Scavenge 和 ParNew 一样,都是多线程、新生代垃圾收集器。但是这两者的目的是不一样的:
-
Parallel Scavenge追求 「CPU 吞吐量」「高吞吐量」则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
-
ParNew追求「降低用户停顿时间」「降低用户停顿时间」是以牺牲吞吐量和新生代空间来换取的。(新生代空间变小,垃圾回收变得频繁,导致吞吐量下降)
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
提高吞吐量的利弊
追求「高吞吐量」,可以减少 GC 执行实际工作的时间,高效率的利用CPU的时间
然而,仅仅偶尔运行 GC 意味着每当 GC 运行时将有许多工作要做,因为在此期间积累在堆中的对象数量很高。
单个 GC 需要花更多的时间来完成,从而导致更高的「暂停时间」。而考虑到「低暂停时间」,最好频繁运行 GC 以便更快速完成,反过来又导致「吞吐量」下降。
停顿时间和吞吐量相关参数
-XX:GCTimeRadio设置垃圾回收时间占总 CPU 时间的百分比。-XX:MaxGCPauseMillis设置垃圾处理过程最久停顿时间。-XX:+UseAdaptiveSizePolicy开启自适应策略。我们只要设置好堆的大小和MaxGCPauseMillis或GCTimeRadio,收集器会自动调整新生代的大小、Eden和Survivor的比例、对象进入老年代的年龄,以最大程度上接近我们设置的MaxGCPauseMillis或GCTimeRadio。
适用场景
Parallel Scavenge 追求 CPU 吞吐量,能够在较短时间内完成指定任务,因此适合没有交互的后台计算。
老年代收集器
Serial Old
什么是Serial Old?
Serial Old 收集器是 Serial 的老年代版本,都是单线程收集器,只启用一条 GC 线程。
Serial Old 工作在老年代,使用「标记-整理算法」,Serial 工作在新生代,使用「复制算法」。
适用场景
上一个标题已经介绍了Serial Old 不过是Serial的老年代版本,并且在Client场景下一般是两个一起搭配适用的。
所以Serial Old是适合Client场景的。
如果是在Server场景下也有两个应用场景:
- 在 JDK 1.5 以及之前版本(
Parallel Old诞生以前)中与Parallel Scavenge收集器搭配使用。 - 作为
CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
Parallel Old
什么是Parallel Old?
Parallel Old 收集器是 Parallel Scavenge 的老年代版本,都追求 CPU 吞吐量。
适用场景
在注重吞吐量以及 CPU 资源敏感的场景下,可以优先考虑 Parallel Scavenge + Parallel Old。
CMS
什么是CMS?
CMS(Concurrent Mark Sweep,并发标记清除)收集器是以获取最短回收停顿时间为目标的收集器(追求低停顿)
CMS在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。
Mark Sweep指的就是「标记-清除算法」,所以可以清楚CMS是基于「标记-清除算法」的。
CMS 的执行流程
下图标成蓝色的是CMS线程:
-
初始标记(CMS initial mark)
Stop The World,仅使用一条初始标记线程对所有与GC Roots直接关联的对象进行标记。 -
并发标记(CMS concurrent mark)
不需要停顿,使用多条标记线程,与用户线程并发执行。此过程进行可达性分析,标记出所有废弃对象。速度很慢。
-
重新标记(CMS remark)
Stop The World,使用多条标记线程并发执行,将刚才并发标记过程中新出现的废弃对象标记出来。 -
并发清除(CMS concurrent sweep)
不需要停顿,只使用一条
GC线程,与用户线程并发执行,清除刚才标记的对象。这个过程非常耗时。
为什么说 CMS 可以做到与应用程序线程并发执行?
根本原因在于采用基于「标记-清除」的算法并对算法过程进行了细粒度的分解。以流水线方式拆分了收集周期,将耗时长的操作单元保持与应用线程并发执行。
并发标记与并发清除过程耗时最长,而这个过程中收集器线程可以与用户线程一起工作。因此总体上说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。
CMS 的优点
并发收集,低停顿。
CMS 的缺点
-
吞吐量低
「低停顿时间」是以牺牲「吞吐量」为代价的,导致 CPU 利用率不够高。
-
无法处理浮动垃圾
可能出现
Concurrent Mode Failure。「浮动垃圾」是指「并发清除」阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次GC时才能进行回收。由于「浮动垃圾」的存在,因此需要预留出一部分内存,意味着CMS收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放「浮动垃圾」,就会出现
Concurrent Mode Failure,这时虚拟机将临时启用Serial Old来替代CMS。 -
导致频繁
Full GC「标记 - 清除算法」导致的内存碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次
Full GC。
对于产生内存的问题,可以通过开启
-XX:+UseCMSCompactAtFullCollection,在每次Full GC完成后都会进行一次「内存压缩整理」,将零散在各处的对象整理到一块。设置参数
-XX:CMSFullGCsBeforeCompaction告诉CMS,经过了 N 次Full GC之后再进行一次内存整理。
适用场景
CMS以获取最短回收停顿时间为目标,非常符合在注重用户体验的应用上使用。
通用收集器
Garbage First(G1)
什么是G1?
G1 收集器以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。
HotSpot 开发团队赋予G1的使命是未来可以替换掉 CMS 收集器。
G1没有新生代和老年代的概念,而是将堆划分为一块块独立的 Region(重新定义堆空间) 。也因此我们把G1分类成「通用收集器」。
-
什么是Region?
通过引入
Region的概念,原来一整块内存空间划分成多个的小空间(新生代和老年代不再物理隔离),使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。Region的大小是一致的,数值是在1M到32M字节之间的一个2的幂值数,JVM会尽量划分2048个左右、同等大小的Region。通过记录每个
Region垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个「优先列表」,每次根据允许的收集时间,优先回收价值最大的Region。 -
为什么需要Region?
这么做的目的是在进行收集时不必在全堆范围内进行,这是它最显著的特点。
区域划分的好处就是带来了「停顿时间可预测的收集模型」(用户可以指定收集操作在多长时间内完成)。可以说
G1提供了接近实时的收集特性。G1收集器之所以能建立「停顿时间可预测的收集模型」,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1会通过一个合理的计算模型,计算出每个Region的收集成本并量化。这样收集器在给定了“停顿”时间限制的情况下,总是能选择一组恰当的Regions作为收集目标,让其收集开销满足这个限制条件,以此达到实时收集的目的。由于每次都是回收价值最大的
Region,因此可以获得最大的回收效率。由于侧重点在于回收最大垃圾量的
Region,这个特点让G1获得了Garbage First(垃圾优先)的名字。 -
一个对象和它内部所引用的对象可能不在同一个 Region 中,那么当垃圾回收时,是否需要扫描整个堆内存才能完整地进行一次可达性分析?
并不!每个
Region都有一个Remembered Set,用于记录本区域中所有对象引用的对象所在的区域,进行可达性分析时,只要在GC Roots中再加上Remembered Set即可防止对整个堆内存进行遍历。关于
Remembered Set的内容我在我的博客:从零开始的JVM学习--GC的「分代收集理论」章节中有有介绍
G1的特点
-
并行与并发
G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop The World的时间。部分其他收集器原来需要停顿Java线程执行的
GC操作,G1收集器仍然可以通过并发的方式让Java程序继续运行。
-
分代收集
从分代上看,
G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个
Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。将堆空间分为若干个区域(
Region) , 这些区域中包含了逻辑上的年轻代和老年代。和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代;
-
空间整合
从整体上看,
G1是基于「标记-整理算法」实现的收集器。从局部(两个
Region之间)上看是基于「复制算法」实现的。但无论如何,这两种算法都意味着
G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次
GC。
-
可预测的停顿
这是
G1相对于CMS的一个优势,降低停顿时间是G1和CMS共同的关注点。
G1收集器的工作流程
如果不计算维护 Remembered Set 的操作,G1 收集器的工作过程分为以下几个步骤:
-
初始标记
Stop The World,但是耗时很少。 仅使用一条初始标记线程对所有与GC Roots「 直接关联」的对象进行标记。并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象, -
并发标记
不需要停顿。此过程进行「可达性分析」,找出存活对象。这阶段耗时较长,但可与用户程序并发执行。
-
最终标记
Stop The World,但是可以并行执行。 使用多条标记线程并发执行。是为了修正「并发标记」期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中 -
筛选回收
可以和用户线程并发执行。首先对各个
Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。 因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
相关参数设置
-XX:+UseG1GC指定使用G1收集器。
-XX:G1HeapRegionSize设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。
-XX:MaxGCPauseMillis设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms
-XX:ParallelGCThread设置STW工作线程数的值。最多设置为8
-XX:ConcGCThreads设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。
-XX:InitiatingHeapOccupancyPercent设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。
适用场景
G1是一款面向服务端应用的垃圾收集器。主要针对配备多核CPU,及大容量内存的机器。
如果发现以下特征可以考虑使用G1收集器以追求更佳性能:
- 实时数据占用了超过半数的堆空间;
- 对象分配率或“晋升”的速度变化明显;
- 期望消除耗时较长的
GC或停顿(超过0.5——1秒)。
小结
本篇文章介绍了最经典主流的几款垃圾收集器。根据工作的位置不同对它们进行了分类,并且根据工作特点介绍了它们各自的适用场景。
其他还有很多的垃圾收集器比如Shenandoah,ZGC,AliGC,Zing... 但是由于它们不是那么主流和篇幅原因没有放到本章介绍,以后有机会我也会专门写博客介绍。
本文参考: