在涉及 Java 相关的面试中,面试官经常会让讲讲 Java 中的垃圾收集相关的理解和常见的分类。可见,光就应付面试而言,JVM 的垃圾收集也对每一位 Java 开发者很重要。除此之外,对于我们了解和解决 Java 应用的性能时,也很有帮助。
在上一篇介绍了 Java 虚拟机内存的垃圾收集算法。本章将会介绍 Java 中常用的垃圾收集器及其特性。
本文的主要内容:
在介绍具体的垃圾回收器之前,我们先了解几个基本概念。
计算机系统的信息交换有两种方式:并行数据传输方式和串行数据传输方式。
举个简单的例子,写代码和听音乐这两件事:
读者可以思考上面所述的串行、并行和并发。
在 JVM 垃圾收集器中也涉及到如上的三个概念。
在了解了这些概念之后,我们开始具体介绍常用的垃圾收集器。
如上所述,串行回收器是指使用单线程进行垃圾回收的回收器,每次回收时串行回收器只有一个工作线程,对于并发能力较弱的计算机来说,串行回收器的专注性和独占性往往有更好的表现。串行回收器可以在新生代和老年代使用,根据作用的堆空间不同,分为新生代串行回收器和老年代串行回收器。
Serial收集器是最古老的收集器,它的缺点是当Serial收集器想进行垃圾回收的时候,必须暂停用户的所有进程,即 STW(服务暂停)。到现在为止,它依然是虚拟机运行在 client 模式下的默认新生代收集器。
参数控制: -XX:+UseSerialGC
使用串行收集器。
Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。
UseSerialGC:开启此参数使用 Serial & Serial Old 搜集器(client 模式默认值)。
并行回收器是在串行回收器的基础上做了改进,它可以使用多个线程同时进行垃圾回收,对于计算能力强的计算机来说,可以有效的缩短垃圾回收所需的实际时间。
ParNew 收集器是一个工作在新生代的垃圾收集器,它只是简单的将串行收集器多线程化,它的回收策略和算法和串行回收器一样。新生代并行,老年代串行;新生代复制算法、老年代标记-整理。
参数控制: -XX:+UseParNewGC
使用 ParNew 收集器; -XX:ParallelGCThreads
限制线程数量 除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。
Parallel 是采用复制算法的多线程新生代垃圾回收器,Parallel 收集器更关注系统的吞吐量。所谓吞吐量就是 CPU 用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间/(运行用户代码时间 + 垃圾收集时间)。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能够提升用户的体验;
而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;新生代复制算法、老年代标记-整理
参数控制:
Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,采用多线程和 标记-整理
算法,也是比较关注吞吐量。在注重吞吐量及 CPU 资源敏感的场合,都可以优先考虑 Parallel 加 Parallel Old 收集器。
参数控制: -XX:+UseParallelOldGC
使用 Parallel Old 收集器; -XX:ParallelGCThreads
限制线程数量。
CMS(Concurrent Mark Sweep) 并发标记请除,它使用的是标记-清除法,工作在老年代,主要关注系统的停顿时间。
CMS 并不是独占的回收器,也就是说,CMS 回收的过程中应用程序仍然在不停的工作,又会有新的垃圾不断的产生,所以在使用CMS的过程中应该确保应用程序的内存足够可用,CMS不会等到应用程序饱和的时候才去回收垃圾,而是在某一阀值(默认为68)的时候开始回收,也就是说当老年代的空间使用率达到68%的时候会执行CMS。如果内存使用率增长很快,在CMS执行过程中,已经出现了内存不足的情况,此时,CMS回收就会失败,虚拟机将启动老年代 Serial 进行垃圾回收,这会导致应用程序中断,直到垃圾回收完成后才会正常工作,这个过程GC的停顿时间可能较长,所以阀值的设置要根据实际情况设置。
主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:
标记-清除
标记清除法产生的内存碎片问题,CMS 提供提供了一些优化设置,可以设置完成 CMS 之后进行一次碎片整理,也可以设置进行多少次 CMS 回收后进行碎片整理。
参数控制:
-XX:+UserConcMarkSweepGC -XX:CMSInitatingPermOccupancyFraction -XX:ConcGCThreads -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction
G1(Garbage First) 垃圾收集器是当今垃圾回收技术最前沿的成果之一。早在 JDK7 就已加入 JVM 的收集器大家庭中,成为 HotSpot 重点发展的垃圾回收技术。
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region。包括:Eden、Survivor、Old 和 Humongous。
其中,Humongous 是特殊的 Old 类型,回收空闲巨型分区,专门放置大型对象。这样的划分方式意味着不需要一个连续的内存空间管理对象。G1 将空间分为多个区域,优先回收垃圾最多的区域。一个对象和它内部所引用的对象可能不在同一个 Region 中,那么当垃圾回收时,是否需要扫描整个堆内存才能完整地进行一次可达性分析?
当然不是,每个 Region 都有一个 Remembered Set(已记忆集合),用于记录本区域中所有对象引用的对象所在的区域,从而在进行可达性分析时,只要在 GC Roots 中再加上 Remembered Set 即可防止对所有堆内存的遍历。
同 CMS 垃圾回收器一样,G1 也是关注最小时延的垃圾回收器,也同样适合大尺寸堆内存的垃圾收集,官方也推荐使用 G1 来代替选择 CMS。G1 最大的特点是引入分区的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至 CMS 的众多缺陷。
G1收集器的运作大致分为以下几个步骤:
G1 能充分利用多 CPU、多核环境下的硬件优势,使用多 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿的时间,部分其他收集器原本需要停顿 Java 线程执行的GC动作,G1 收集器仍然可以通过并发的方式让 Java 程序继续执行。
此外,与其他收集器一样,分代概念在G1中依然得以保留。虽然 G1 可以不需其他收集器配合就能独立管理整个 GC 堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次 GC 的旧对象以获取更好的收集效果。
空间整合:与 CMS 的 标记-清理
算法不同,G1 从整体看来是基于 标记-整理
算法实现的收集器,从局部(两个 Region 之间)上看是基于 复制
算法实现,无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。
可预测的停顿:这是 G1 相对于 CMS 的另外一大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒,这几乎已经是实时 Java(RTSJ)的垃圾收集器特征了。
参数控制: -XX:+UseG1GC
。
本文介绍了常见的7种不同分代的收集器:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;而它们所处区域,则表明其是属于新生代收集器还是老年代收集器:
根据收集的区域(年轻代或年老代)和收集器自身的特性,可以有如下组合: Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel/Serial Old、Parallel/Parallel Old、G1。
ZGC 来了。ZGC 是 JDK11 中要发布的最新垃圾收集器。完全没有分代的概念,官方给出 ZGC 的优点是无碎片,时间可控,超大堆。读者可以尝试了解和使用一下 ZGC 。