垃圾收集器
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
- 单线程与多线程: 单线程垃圾收集方式指的是利用单一的线程来执行垃圾回收任务。相比之下,多线程垃圾收集则通过多个线程并行工作,加速了垃圾回收的过程,尤其在多核处理器环境中更为高效。
- 串行与并行:在垃圾收集的上下文中,串行执行意味着垃圾收集器的工作与应用程序的执行是交替进行的。具体而言,当垃圾收集进程运行时,需要暂停(或称为“停顿”)用户程序,待垃圾回收完成后再恢复应用程序的执行。这种模式在早期Java虚拟机中较为常见,适用于内存较小、CPU资源有限的环境。而并行执行模式,则允许垃圾收集器与用户程序并发执行,即垃圾收集操作不必完全停止应用程序。这样可以减少垃圾回收引起的程序停顿时间,提高整体系统的响应速度。CMS和 G1 收集器是Java平台中采用并行处理的代表,它们通过复杂的算法设计实现了垃圾收集的同时,保持了应用程序的持续运行,特别适合对延迟敏感的服务场景。
| 收集器 | 串行、并行or并发 | 新生代/老年代 | 算法 | 目标 | 适用场景 |
|---|---|---|---|---|---|
| Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单CPU环境下的Client模式 |
| ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境时在Server模式下与CMS配合 |
| Parallel Scavenge | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
| Serial Old | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单CPU环境下的Client模式、CMS的后备预案 |
| Parallel Old | 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
| CMS | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或B/S系统服务端上的Java应用 |
| G1 | 并发 | both | 标记-整理+复制算法 | 响应速度优先 | 面向服务端应用,替换CMS |
1. 串行 GC(Serial GC)
Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK 1.3.1之前)是HotSpot虚拟机新生代收集器的唯一选择。大家只看名字就能够猜到,这个收集器是一个单线程工作的收集器,但它的“单线 程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。“Stop The World”这个词语也许听起来很酷,但这项工作是由虚拟机在后台自动发起和自动完成的,在用户不可知、不可控的情况下把用户的正常工作的线程全部停掉,这对很多应用来说都是不能接受的。不妨试想一下,要是你的电脑每运行一个小时就会暂停响应五分钟,你会有什么样的心情? 下图示意了Serial/Serial Old收 集器的运行过程。
它是 Client 模式下的默认新生代收集器,因为在用户的桌面应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。
对于内存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint)最小的;对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。在用户桌面的应用场景以及近年来流行的部分微服务应用中,分配给虚拟机管理的内存一般来说并不会特别大,收集几十兆甚至一两百兆的新生代(仅仅是指新生代使用的内存,桌面应用甚少超过这个容量),垃圾收集的停顿时间完全可以控制在十几、几十毫秒,最多一百多毫秒以内,只要不是频繁发生收集,这点停顿时间对许多用户来说是完全可以接受的。
要启用此款收集器,只需要指定一个 JVM 启动参数即可,同时对年轻代和老年代生效:
-XX:+UseSerialGC
该选项只适合几百 MB 堆内存的 JVM,而且是单核 CPU 时比较有用。
对于服务器端来说,因为一般是多个 CPU 内核,并不推荐使用,除非确实需要限制 JVM 所使用的资源。大多数服务器端应用部署在多核平台上,选择 串行 GC 就意味着人为地限制了系统资源的使用,会导致资源闲置,多余的 CPU 资源也不能用增加业务处理的吞吐量。
2. ParNew 收集器
它是 Serial 收集器的多线程版本。
ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处,但它却是不少运行在服务端模式下的HotSpot虚拟机,尤其是JDK 7之前的遗留系统中首选的新生代收集器,其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有它能与CMS 收集器配合工作。
在JDK 5发布时,HotSpot推出了一款在强交互应用中几乎可称为具有划时代意义的垃圾收集器 ——CMS收集器。这款收集器是HotSpot虚拟机中第一款真正意义上支持并发的垃圾收集器,它首次实现了让垃圾收集线程与用户线程(基本上)同时工作。
遗憾的是,CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器ParallelScavenge配合工作,所以在JDK 5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者 Serial收集器中的一个。ParNew收集器是激活CMS后(使用-XX:+UseConcMarkSweep GC选项)的默认新生代收集器,也可以使用-XX:+/-UseParNewGC选项来强制指定或者禁用它。
可以说直到CMS的出现才巩固了ParNew的地位,但成也萧何败也萧何,随着垃圾收集器技术的不断改进,更先进的G1收集器带着CMS继承者和替代者的光环登场。G1是一个面向全堆的收集器,不再需要其他新生代收集器的配合工作。所以自JDK 9开始,ParNew加CMS收集器的组合就不再是官方推荐的服务端模式下的收集器解决方案了。官方希望它能完全被G1所取代,甚至还取消了ParNew加 Serial Old以及Serial加CMS这两组收集器组合的支持(其实原本也很少人这样使用),并直接取消了- XX:+UseParNewGC参数,这意味着ParNew和CMS从此只能互相搭配使用,再也没有其他收集器能够和它们配合了。读者也可以理解为从此以后,ParNew合并入CMS,成为它专门处理新生代的组成部分。
HotSpot虚拟机里面关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法的,而关注延迟的CMS收集器则是基于标记-清除算法的。
3. Parallel Scavenge 收集器
与 ParNew 一样是多线程收集器。
Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。Parallel Scavenge的诸多特性从表面上看和ParNew非常相似,那它有什么特别之处呢?
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值, 即:
可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手动指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
通过命令行参数-XX:ParallelGCThreads=NNN来指定 GC 线程数,其默认值为 CPU 核心数。可以通过下面的任意一组命令行参数来指定并行 GC:
-XX:+UseParallelGC
-XX:+UseParallelOldGC
-XX:+UseParallelGC -XX:+UseParallelOldGC
Parallel Scavenge和ParNew的差异
Parallel Scavenge和ParNew都是Java虚拟机中用于新生代的垃圾收集器,它们都采用了多线程的复制算法来提高垃圾回收的效率。尽管它们有相似之处,但也存在一些关键差异:
Parallel Scavenge收集器
- 设计目标: Parallel Scavenge收集器的主要设计目标是达到可预测的吞吐量。吞吐量定义为应用程序运行时间与应用程序运行时间加上垃圾收集时间之比。这意味着它更加关注于在整体上最大化应用程序的执行效率,而不是个别垃圾收集循环的最短停顿时间。
- 调节参数: 它提供了几个独特的调节选项,如 -XX:MaxGCPauseMillis 和 -XX:GCTimeRatio,允许用户指定最大垃圾收集停顿时间或者期望的垃圾收集时间占总时间的比例,从而让JVM动态调整新生代大小、晋升阈值等参数,以达到用户设定的吞吐量目标。
ParNew收集器
- 设计目标: ParNew实质上是Serial收集器的多线程版本,它的主要目的是与CMS(Concurrent Mark Sweep)收集器配合使用,以提供一个低延迟的解决方案。与Parallel Scavenge相比,ParNew更侧重于减少新生代垃圾收集的停顿时间,而不是最大化整体吞吐量。
- 默认配置: 在某些JDK版本中,ParNew曾是CMS收集器默认搭配的新生代收集器。不过,随着技术发展,Parallel Scavenge和Parallel Old的组合逐渐成为更推荐的选择,尤其是在JDK 9以后,ParNew逐渐被废弃,最终在JDK 14中完全移除。
- 调优选项: ParNew提供的调优选项相对较少,主要是关于线程数量的控制,不像Parallel Scavenge那样可以直接调整以达到特定的吞吐量或停顿时间目标。
4. Serial Old 收集器
Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS(Concurrent Mark Sweep)和G1(Garbage First)收集器的备选预案。
什么叫CMS(Concurrent Mark Sweep)和G1(Garbage First)收集器的备选预案?
在某些特殊情况下,如CMS收集器发生 Concurrent Mode Failure 或者G1收集器进行Full GC时,可能回退到使用Serial Old收集器进行老年代的垃圾回收。
- CMS收集器的Concurrent Mode Failure: 当CMS收集器在并发标记阶段发现老年代剩余空间不足以容纳即将晋升的对象时,会发生Concurrent Mode Failure。这时,为了防止内存溢出,JVM会放弃当前的并发收集,转而启动一次完整的Stop-The-World的Full GC,通常会使用Serial Old收集器来执行这次老年代的清理工作。这是因为Serial Old收集器虽然会导致较长时间的停顿,但它是最稳定且对内存条件要求最低的收集器之一,能够确保在紧急情况下成功回收足够的空间。
- G1收集器进行Full GC: G1收集器设计初衷是在大多数情况下避免全区域的Stop-The-World事件,但当内存回收压力极大,比如分配速率远超回收速率,或者达到预设的触发条件(如Humongous对象分配失败),G1也可能执行Full GC。在这种情况下,如果G1判断标准的并发清理过程无法及时释放所需内存,有可能会选择fallback到使用Serial Old收集器来进行老年代的清理。这同样是为了确保在极端条件下系统的稳定性,尽管这样做会牺牲一部分性能,导致较长的停顿时间。
这些机制体现了JVM在处理复杂内存管理时的灵活性和鲁棒性,确保即使在面临挑战时也能维持系统的正常运行
在采用CMS(Concurrent Mark Sweep)垃圾回收器时,其中一个触发垃圾回收的关键条件是老年代内存占用达到预设阈值。这一阈值可通过参数-XX:CMSInitiatingOccupancyFraction进行配置。在JDK 1.6环境中,默认设定为92%,意味着当老年代空间使用率达到92%时,CMS机制会自动启动,旨在执行垃圾回收操作,以此保留8%的空间作为缓冲。这预留的空间旨在应对CMS回收过程中的第二和第三个阶段,期间虽不进行全局停顿(Stop-The-World),但应用程序仍持续运行,可能导致部分新对象晋升至老年代。因此,这8%的预留不仅是为了确保CMS回收过程的顺畅进行,也是为了容纳并发标记清除阶段新产生的老年代对象,从而维持系统的稳定运行与性能。
那么如果CMS垃圾回收期间,系统程序要放入老年代的对象大于了可用内存空间,此时会如何?
这个时候,会发生Concurrent Mode Failure,就是说并发垃圾回收失败了,我一边回收同时你一边把对象放入老年代,发现内存都不够了。此时就会自动用“Serial Old”垃圾回收器替代CMS,就是直接强行把应用程序“Stop the World”,重新进行长时间的GC Roots追踪,标记出来全部垃圾对象,不允许新的对象产生。然后一次性把垃圾对象都回收掉,完事儿了再恢复系统线程。
所以在生产实践中,这个自动触发CMS垃圾回收的比例需要合理优化一下,避免“Concurrent Mode Failure”问题
- 单线程收集器:Serial Old收集器和它的新生代伙伴Serial收集器一样,都是采用单线程的方式进行垃圾回收工作。这意味着在执行垃圾收集时,它会暂停所有其他的工作线程(Stop-The-World),直到老年代的垃圾回收工作完成。
- 简单高效:由于是单线程操作,避免了多线程的协调开销,实现简单。对于单处理器或者处理器核心较少的环境来说,Serial Old收集器由于没有线程交互的开销,可以提供较好的收集效率。
- 使用场景:主要适用于对响应时间要求不高、处理器资源有限,并且不需要太大吞吐量的环境。例如在一些嵌入式系统或者对暂停时间要求不严格的桌面应用中可能会选择使用。
- 内存占用:Serial Old收集器在运行过程中内存占用较小,这是因为它不需要为多个线程分配内存空间来维护数据结构。
- 与其他收集器搭配:在某些特殊情况下,如CMS收集器发生 Concurrent Mode Failure 或者G1收集器进行Full GC时,可能回退到使用Serial Old收集器进行老年代的垃圾回收。
5. Parallel Old 收集器
是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。这个收集器是直到JDK 6时才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于相 当尴尬的状态,原因是如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器以外别无选择,其他表现良好的老年代收集器,如CMS无法与它配合工作。由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用Parallel Scavenge收集器也未必能在整体上获得吞吐量最大化的效果。同样,由于单线程的老年代收集中无法充分利用服务器多处理器的并行处 理能力,在老年代内存空间很大而且硬件规格比较高级的运行环境中,这种组合的总吞吐量甚至不一 定比ParNew加CM S的组合来得优秀。
直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。Parallel Old收集器的工作过程如图3-10所示。