JVM 的垃圾收集器:Serial、ParNew 、 Parallel Scavenge、Serial Old、Parallel Old、CMS 、G1 。
1. 概述
下图中垃圾收集器只要连线则表明垃圾收集器可以搭配使用。没有万能的收集器,具体应用选择合适的垃圾收集器。
2. Serial垃圾收集器
单线程收集器,用于新生代,这里的单线程指的是它在进行垃圾回收时暂停其他工作线程(STOP THE WORLD)。
缺点:单线程,暂停其他工作线程,停顿过长时间难以忍受。
优点:简单高效(没有线程切换的开销),适用用户的桌面应用场景,停顿较短且不频繁。
2. ParNew垃圾收集器
Serial垃圾收集器的多线程版本,多条线程去进行垃圾收集。
多核 CPU 收集效果才会高于 Serial垃圾收集器。
它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使用。
3. Parallel Scavenge垃圾收集器
Parallel Scavenge垃圾收集器用于新生代 , 其目的是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器。
高吞吐量表明可以高效率地利用 CPU,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。交互多的场景需要停顿时间短,才能保证用户体验更好。
吞吐量 = 代码运行时间 / (代码运行时间 + 垃圾收集时间)。
Parallel Scavenge 有两个控制停顿时间(-XX:MaxGCPauseMillis)和设置吞吐量大小(-XX:GCTimeRatio)的参数。
代价:降低停顿时间则会降低吞吐量,因为降低停顿时间意味着调低新生代内存,这样垃圾收集次数变的频繁。
Parallel Scavenge 可以自适应调节,打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
4. Serial Old 垃圾收集器
Serial垃圾收集器 的老年代版,使用标记-整理算法。
作为 CMS 的 预备方案,在 CMS 发生 Concurrent Mode Failure 的时候使用。
5. Parallel Old垃圾收集器
Parallel Scavenge 垃圾收集器的老年代版本,使用多线程 和 标记-整理算法。
Parallel Scavenge+Parallel Old 适用于 注重吞吐量 和 CPU 资源敏感的 场合。
6. CMS 垃圾收集器
CMS 垃圾收集器,其目标是获取最短回收停顿时间,例如:B/S系统的服务端,重视响应速度,希望系统停顿时间最短。
CMS 收集器基于“标记-清除”算法实现,其运作过程:
(1)初始标记 (CMS initial mark)。标记和GC Roots 能直接关联到的对象,速度很快。
(2)并发标记(CMS concurrent mark)。耗时最长,可以和用户线程一起工作。
(3)重新标记(CMS remark)。修正并发标记期间因用户继续运作而导致标记产生变动的那一部分对象的标记记录。时间会稍长于初始标记,远短于并发标记。
(4)并发清除(CMS concurrent sweep)。耗时最长,可以和用户线程一起工作。
初始标记 和 重新标记是单线程。
CMS 垃圾收集器的缺点:
(1)在并发阶段会占用一部分 CPU 资源,导致应用程序变慢,总吞吐量会降低。
如果CPU太少就会导致用户程序的执行速度大幅度降低,虚拟机为了处理这种情况,提出了 增量式并发收集器 i-CMS,即在并发标记-清理的时候让 GC 线程、用户线程交替运行,尽量减少 GC 线程的独占资源时间,用户程序的影响会降低,但是会导致垃圾收集过程变长。实践证明 i-CMS 效果一般。
(2)无法处理浮动垃圾,CMS并发清理阶段,程序任在运行,会不断产生新的垃圾(浮动垃圾),这些垃圾只能在下一次GC中才能清理,此时还需要保障有足够的内存空间给线程使用,因此CMS需要预留一部分空间,避免浮动垃圾过多,JDK5的默认设置中,CMS老年代使用了 68%的内存就会被激活(进行回收),JDK1.6 中,启动阈值已经提升到 92%,在 垃圾清理时如果预留的内存无法满足程序的需要,就会临时启用 Serial Old 收集器(暂停执行程序)。
可以通过修改 -XX:CMSInitiatingOccupancyFraction 的值来修改阈值。
(3)CMS 是基于标记-清除算法,会导致有大量空间碎片。
当无法找到足够大的连续空间分配当前对象,就会提前触发一次 Full GC(可以对内存碎片合并整理),内存整理无法并发,会导致停顿时间过长,可以通过XX:CMSFullGCsBeforeCompaction这个参数,设置执行多少次不对内存碎片合并整理的 Full GC , 跟着来一次内存碎片合并整理的Full GC。
7. G1 垃圾收集器
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。
HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
G1 (Garbage-First)的特点:
(1)并行和并发:充分利用多CPU、多核环境下的硬件优势,使用多CPU来缩短 STOP THE WORLD 的时间,部分其他收集器原本需要停顿Java线程执行的GC操作,G1收集器任然可以通过并发的方式让Java程序继续执行。
(2)分代收集:与其他收集器 一样,分代概念在G1中依然得以保留。
(3)空间整合。整体来看是标记整理算法,从局部来看(两个Region)上来看是基于“复制算法”。
(4)可预测的停顿。垃圾收集器的停顿时间不得超过一定时间。
G1 将整个 Java 堆划分为多个大小大的 独立区域 (Region),虽然还保留新生代和老年代的概念,新生代和老年代不再是物理隔离,他们都是一部分Region 。
G1 收集器之所以能建立可预测的停顿时间模型,因为它避免Java堆全区域的垃圾收集,G1 跟踪各个 Region 里面的垃圾堆积的价值大小,回收所获得的空间大小以及回收所需的经验值,在后台维护一个优先列表,每次根据允许的垃圾收集时间回收价值最大的Region(Garbage-First) 。
一个问题:Region不可能是孤立的,一个对象分配在一个Region 中,他会和在其他的Region 中的对象有引用关系,那么在做可达性判断的时候,还是需要扫描整个Java堆。
每个集合都有一个 Remembered SET。
如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
初始标记, 并发标记,最终标记,筛选回收
初始标记:仅仅标记GC Roots能够直接关联到的对象。
并发标记:从GC Roots 对堆中的对象进行可达性分析。
最终标记是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
筛选回收首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
参考文献
- 深入理解Java虚拟机:JVM高级特性与最佳实践 / 周志明著. —— 2 版 . —— 北京:机械工业出版社,2013.6