概述
在之前的文章中,我们介绍了 java 虚拟机内存回收的基本算法和原理,本文中,我们着重介绍一下包含在 jdk1,7 以后的 HotSpot 虚拟机中的垃圾收集器。
如下图所示,HotSpot 中包含了下列的 7 个收集器:
图中涉及两个概念: 1. 并行(Parallel) — 多个垃圾收集线程可以并行工作,但用户线程处于等待状态 2. 并发(Concurrent) — 用户线程与收集线程可以同时执行
Serial
Serial 是最基本和最早的收集器。 他是一个单线程收集器,在他收集的同时,必须暂停其他的全不工作,直到收集结束,这样虽然给用户带来了不良的体验,但是在垃圾收集的效率和效果上的表现都十分优秀。 在桌面应用中,分配给虚拟机的内存一般来说斌不大,收集工作通常只需要十毫秒或几十毫秒,这样的停顿是可以接受的,因此 Serial 收集器是 Client 模式下虚拟机的一个很好的选择。
通过下列参数可以控制收集器的行为:
- -XX:SurvivorRatio — Eden 区与 Survivor 区的大小比值,如果设置为 8,则两个 Survivor 区与一个 Eden 区的比值为 2:8
- -XX:PretenureSizeThreshold — 对象超过多大时直接在老年代分配和创建
- -XX:MaxTenuringThreshold — 对象超过多少次 Minor GC(新生代垃圾收集)没有被回收就自动放入老年代
- -XX: HandlePromotionFailure — 关闭新生代收集担保
ParNew
ParNew 是一个并行收集器,他是 Serial 的并行版本,它使用了多个线程进行垃圾收集。 他可以使用 Serial 的所有参数控制它的行为。 虽然和 Serial 相比,除了可以并行收集,在收集过程中还是同样会暂停所有线程,但他却是许多运行在 server 模式下的虚拟机中首选的新生代收集器,一个重要原因就是除 Serial 外,他是唯一能和 CMS 收集器搭配使用的新生代收集器。
通过 -XX: UseConcMarkSweepGC 或 -XX: UseParGC 选项指定了使用 ParNew 收集器进行新生代的收集器。 通过 -XX:ParallelGCThreads 可以指定垃圾收集器的线程数。
Parallel Scavenge
与 ParNew 一样,Parallel Scavenge 也是一个使用复制算法的多线程并行收集器。 Parallel Scavenge 收集器的目标是可控制吞吐量(吞吐量 = 运行用户代码时间/(运行用户代码时间 垃圾收集时间),如果 jvm 运行了 100 分钟,其中垃圾收集花费了 1 分钟,吞吐量就是 99%) 高的吞吐量可以让 CPU 得到更高效的利用,让运算尽快完成,这在服务端运行的非交互式程序十分有用。
通过 -XX:MaxGCPauseMillis 参数可以控制收集的停顿时间,参数是一个大于 0 的毫秒数。 通过 -XX:GCTimeRatio 参数可以设置吞吐量大小(大于 0 小于 100) 通过 -XX: UseAdaptiveSizePolicy 控制是否自动根据当前系统情况决定最大吞吐量的限制。
Serial Old
Serial Old 是 Serial 的老年代版本,同样是一个单线程收集器。 它使用标记-整理算法。 主要用在 client 模式下,也用于在 server 模式下和 Parallel Scavenge 搭配使用,通常,他也作为 CMS 收集器在发生 Concurrent Mode Failure 时使用的后备预案。
Parallel Old
Parallel Old 是 Parallel Scavenge 收集器,JDK 1.6 才提供,因此在注重吞吐量的场景下,Parallel Scavenge 与 Serial Old 的组合再也不是唯一的选择了,更好地选择是 Parallel Scavenge 与 Parallel Old 的组合。
CMS
CMS 即 Concurrent Mark Sweep 收集器,是一种以获取最短回收停顿时间为目标的收集器。 服务的响应时间决定了用户体验,因此在互联网网站等重视响应速度的场景中,CMS 是非常合适的选择。 CMS 是使用标记-清除算法实现的,整个收集过程分为四步: 1. 初始标记 — 这一步工作中需要暂停所有线程,他标记所有 GC Roots 能够直接关联到的对象,因此速度非常快 2. 并发标记 — 进行 GC RootsTracing 也就是可达性判断 3. 重新标记 — 修正并发标记期间因为用户程序继续运作而导致变动的对象的标记记录,也会造成停顿 4. 并发清除 — 清除内存中无用的对象
由于在清理过程中使用并发标记和并发清除,可以和用户线程同步并发执行,因此它实现了低停顿并发收集的优点。
但是 CMS 还具有以下三个缺点: 1. 清理线程占用一部分线程(CPU 资源)造成程序吞吐量下降,尤其对于 CPU 不足 4 个时,CMS 对用户程序的影响会非常大 2. 无法处理浮动垃圾,可能出现 Concurrent Mode Failure 失败,所谓的浮动垃圾,就是在清理过程中同步产生的新的垃圾,这部分垃圾只能等到下次垃圾收集时才能得到回收,如果 CMS 运行期间预留的内存无法满足程序需要就会出现 Concurrent Mode Failure,性能反而降低 3. 正如我们前面所介绍的,标记清除算法会造成内存碎片的产生,造成老年代空间的浪费甚至无法使用
由于上面的缺点,在 JDK1.5 的默认设置中,CMS 收集器会在老年代使用了 68% 的空间后被自动激活,这样仍留有大量空间,避免了 Concurrent Mode Failure 的发生,在 JDK1.6 的默认设置中,这个阈值被提高到 92%,你可以使用 -XX:CMSInitiatingOccupancyFraction 来设置触发百分比。 通过 -XX: UseCMSCompactAtFullCollection 开关决定在 CMS 收集后是否需要一次内存碎片的整理过程,这个开关默认是开启的。 -XX:CMSFullGCsBeforeCompaction 可以设置执行多少次不压缩的 Full GC 后跟着来一次带压缩的,默认为 0。
G1
G1 收集器即 Garbage-First 收集器,是当今最先进的收集器之一,是 JDK1.7 开始提供的一款面向服务端应用的垃圾收集器。 他拥有以下优点: 1. 并行与并发 — 可以充分利用多 CPU、多核环境下的硬件优势,实现收集的并行与并发,让 java 线程持续运行不受到影响 2. 分代收集 3. 空间整合 — G1 从整体来看使用标记-整理算法实现,在两个 Region 内是使用标记-赋值算法实现,不会在运行期间产生内存碎片 4. 可预测的停顿 — G1 出了追求低停顿外,还能建立可预测的停顿时间模型,能够让使用者明确指定在一个长度为 M 毫秒的时间片段内,垃圾收集的时间不超过 N 毫秒
G1 收集器将整个 java 堆内存划分为多个大小相等的独立区域(Region),新生代与老年代再也不是物理隔离的了,新生代和老年代都是一部分 Region 的集合。 G1 收集器根据收集的经验值可以判断每个 Region 里垃圾收集的价值,从而决定垃圾收集在每个 Region 中的优先级,保证了有限时间内的收集效率。