JVM 中的垃圾收集器详解
一、背景介绍
在之前的几篇文章中,我们介绍了 JVM 内部布局、对象的创建过程、运行期的相关优化手段以及垃圾对象的回收算法等相关知识。
今天通过这篇文章,结合之前的知识,我们一起来了解一下 JVM 中的垃圾收集器。
二、垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
不同的虚拟机所提供的垃圾收集器可能会有很大差异,以 HotSpot 虚拟机为例,所包含的垃圾收集器可以用如下图来概括。
上图中的连线表示,不同分代的收集器可以搭配使用。
- 新生代收集器:Serial、ParNew、Parallel Scavenge
- 老年代收集器:Serial Old、CMS、Parallel Old
- 通用收集器: G1
在虚拟机中,没有所谓的万能收集器,只有根据具体的业务场景,选择最合适的收集器。这也是为什么 HotSpot 实现了这么多收集器的原因。
下面我们一起来看看相关的具体实现。
2.1、Serial 和 Serial Old收集器
Serial 系列的垃圾收集器是 JVM 的第一款收集器,它的设计思路很简单,在新生代,使用单线程采用复制算法进行收集对象;在老年代,使用单线程采用标记整理算法进行收集对象;垃圾收集的过程中会暂停用户线程,直到垃圾收集完毕。
因为当时的硬件环境配置都不高,内存都是几十兆,CPU 也都是单核的,不像现在这样处处都是高并发的应用场景。限于当时的硬件资源和应用场景,这个收集器优势很突出,简单高效、消耗资源也很少。
唯一的不足在于,在用户不可见的情况下要把用户正常工作的线程全部停掉,这对很多应用比较难以接受。不过实际上到目前为止,Serial 收集器依然是虚拟机在 Client 模式下运行的默认新生代收集器,因为它简单而高效。客户端应用模型下,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代对象,停顿时间平均在几十毫秒,只要不是频繁收集,完全可以接受。
整个流程,可以用如下图来概括。
总结下来,收集器特点如下:
- 收集区域: Serial(新生代),Serial Old(老年代)
- 收集算法: Serial(复制算法),Serial Old(标记整理算法)
- 收集方式:单线程
- 优势:简单高效,内存资源占用少,单核 CPU 环境最佳选项
- 劣势:整个搜集过程需要停顿用户线程,多核 CPU、大内存的环境,资源优势无法发挥起来
2.2、ParNew收集器
ParNew 收集器,可以看成是 Serial 收集器的多线程版本。除了使用多线程进行垃圾收集外,其余行为和 Serial 收集器完全一样,包括使用的也是复制算法,垃圾收集时暂停用户线程。在多核 CPU 资源环境下,可以显著提升整个垃圾收集的性能,也是虚拟机在 Server 模式下运行的首选新生代收集器。
能让 ParNew 出名的一个核心因素是,它是除了 Serial 收集器外,目前唯一一个能与 CMS 收集器配合一起使用的新生代收集器,因为 CMS 优秀所以 ParNew 也出名了,有点类似碰上了大款的感觉,其中 CMS 收集器是一款几乎可以认为有划时代意义的垃圾收集器,下文我们再讲。
其次,ParNew 收集器在单个 CPU 的环境中绝对不会有比 Serial 收集器更好的效果,甚至由于线程交互的开销,该收集器在两个 CPU 的环境中都不能百分之百保证可以超越 Serial 收集器。当然,随着可用 CPU 数量的增加,它对于垃圾收集的效率提升还是很有帮助的。
整个流程,可以用如下图来概括。
总结下来,收集器特点如下:
- 收集区域:新生代
- 收集算法:复制算法
- 收集方式:多线程
- 优势:多线程收集,多核 CPU 环境下效率要比 serial 高,新生代中,除了 Serial 收集器外目前唯一一个能与 CMS 配合的收集器
- 劣势:整个搜集过程需要停顿用户线程
2.3、Parallel Scavenge 和 Parallel Old收集器
Parallel Scavenge 和 ParNew 收集器很类似,也是一款使用多线程采用复制算法的新生代收集器;Parallel Old 收集器是一款使用多线程采用标记整理算法的老年代收集器;垃圾收集过程中也会暂停用户线程,直到整个垃圾收集过程结束。
不同的是,Parallel 收集器更关注系统的吞吐量,也被称为“吞吐量优先收集器”。
所谓吞吐量的意思就是 CPU 用于运行用户代码时间与 CPU 总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),比如虚拟机总运行 100 分钟,垃圾收集 1 分钟,那吞吐量就是 99%。高吞吐量可以高效率的利用 CPU 资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
自适应调节策略也是 Parallel Scavenge 与 ParNew 的一个重要区别,用户可以通过参数来打开自适应调节策略,比如-XX:+UseAdaptiveSizePolicy
参数,打开之后就不需要手动指定新生代大小、Eden 区和 Survivor 参数等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量。如果对于垃圾收集器运作原理不太了解,优化比较困难的情况下,使用 Parallel 收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成也是一个不错的选择。
另外,Parallel 收集器是虚拟机在 Server 模式下运行的默认垃圾收集器。
整个执行流程,跟 ParNew 收集器类似。
总结下来,收集器特点如下:
- 收集区域:Parallel Scavenge(新生代),Parallel Old(老年代)
- 收集算法:Parallel Scavenge(复制算法),Parallel Old(标记整理算法)
- 收集方式:多线程
- 优势:多线程收集,多核 CPU 环境下效率要比 serial 高
- 劣势:整个搜集过程需要停顿用户线程
2.4、CMS收集器
CMS 收集器是一种以获取最短回收停顿时间为目标的老年代收集器。
与前面几个收集器不同,它采用了一种全新的策略可以在垃圾回收过程中的某些阶段用户线程和垃圾回收线程一起工作,从而避免了因为长时间的垃圾回收而使用户线程一直处于等待之中。
目前很大一部分 Java 应用集中在互联网站或者 B/S 系统的服务端上,这类应用尤其注重服务的响应速度,希望系统停顿时间最短,比如在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过多少毫秒,以期给用户带来较好的体验,其中 CMS 收集器就非常符合这类应用的需求。
CMS 的英文全程是:Concurrent Mark-Sweep Collector,从名字上就能看出 CMS 收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为如下 4 个步骤:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
CMS 会根据每个阶段不同的特性来决定是否停顿用户线程。整个流程,可以用如下图来概括。(图片来自于勤劳的小手 - 垃圾收集器文章)
2.4.1、阶段一:初始标记
初始标记阶段的工作主要是标记一下 GC Roots 能直接关联到的对象,这个过程会短暂的停顿用户线程,因为并不会对整个 GC Roots 的引用进行遍历,因此速度很快。
2.4.2、阶段二:并发标记
并发标记阶段的工作主要是把阶段一标记好的 GC Roots 对象进行深度的遍历,找到所有与 GC Roots 关联的对象并进行标记,这个过程会采用多线程的方式进行遍历标记,因为非常耗时,CMS 考虑到为了尽量不停顿用户线程,因此这个阶段不会暂停用户线程,也就是说,此时 JVM 会分配一些资源给用户线程执行任务,通过这样的方式减少用户线程的停顿时间。
2.4.3、阶段三:重新标记
重新标记阶段的工作主要是修补阶段二用户线程运行期间产生新的垃圾对象,进行重新标记,同样也是采用多线程方式进行,此阶段数量不会很多,会短暂的停顿用户线程,速度也很快。
2.4.4、阶段四:并发清除
并发清除阶段的工作主要是对那些被标记为可回收的对象进行清理,在一般情况下,并发清除阶段是使用的是“标记-清除”算法,因为这个过程不会牵扯到对象的地址变更,所以 CMS 在并发清除阶段是不需要停止用户线程的,对象回收效率非常高。
与此同时,正因为并发清除阶段用户线程也可以同时运行,所以在用户线程运行的过程中自然也会产生新的垃圾对象,这也就是导致 CMS 收集器会产生“浮动垃圾”的原因,此时也会产生很多的空间碎片,当空间碎片到达了一定程度时,此时 CMS 就会使用“标记-整理”算法来解决空间碎片的问题。
在上文的垃圾回收算法中我们有说到,“标记-整理”算法会将对象的位置进行挪动并更新对象的引用的指向地址,在这个过程中,如果用户线程同时运行的话会产生并发问题,因此当 CMS 进行碎片整理的时候必须得停止用户线程。所以,在某些情况下,并发清除阶段 CMS 也会停顿用户线程。
CMS 收集器作为一个全新思路的垃圾收集器,虽然很优秀,但一直没有被 Hospot 虚拟机纳入为默认的垃圾收集器。时至今日,JDK1.8 使用的默认收集器都还是 Parallel scavenge 和 Parallel old 收集器,主要原因在于 CMS 存在一些比较头疼的问题,比如浮动垃圾、空间碎片整理时会造成系统卡顿、在并发清除阶段可能会出现系统长时间的假死。
2.4.5、小结
总结下来,收集器特点如下:
- 收集区域:老年代
- 收集算法:标记清除算法 + 标记整理算法
- 收集方式:多线程
- 优势:多线程收集过程中可以做到不停止用户线程,以获取最短回收停顿时间
- 劣势:会产生浮动垃圾、空间碎片整理时会造成系统卡顿、并发清除阶段可能会出现系统假死等问题
2.5、G1收集器
G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一,从 JDK 7 Update 4 后开始进入商用。
在 G1 收集器出现之前,不管是 Serial 系列,Parallel 系列,还是 CMS 收集器,它们都是基于把内存进行物理分区的形式将 JVM 内存分成新生代、老年代、永久代或 MetaSpace,这种分区模式下进行垃圾收集时必须对某个区域进行整体性的收集,比如整个新生代、整个老年代收集或者整个堆,当内存空间不大的时候,比如几个 G,通过参数优化能取得不错的收集性能。但是,随着硬件资源的发展,JVM 可用内存从几十 G 到几百 G 甚至上 T 时,这种采用传统模式下的物理分区进行收集时,每次扫描内存的区域自然就变大了,进行垃圾清理的时间自然就变得更长了,此时传统的收集器即时再怎么优化,也难以取得令人满意的收集效果,因此需要一款全新的垃圾收集器。
G1 收集器就是在这样的环境下诞生的,它摒弃了原来的物理分区,把整个 Java 堆分成若干个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离,它们都是一部分 Region 的集合。从结构上看,G1 收集器不要求整个新生代或者老年代都是连续的,也不再坚持固定大小和固定数量,它会跟踪各个 Region 里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。这种通过 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率。
G1 收集器内存划分,可以用如下图来概括。(图片来自于勤劳的小手 - 垃圾收集器文章)
在 G1 收集器里面维护了一个 Collect Set 集合,这个集合里面记录了待回收的 Region 区域信息,同时也包括了每个 Region 区域可回收的大小空间。通过 Collect Set 里面的信息,G1 在进行垃圾收集时,可以根据用户设定的可接受停顿时间来进行分析,在设定的时间范围内优先收集垃圾最多的 Region 区域,以实现高吞吐、低停顿的收集效果。
在工作流程上,G1 收集器也吸收了 CMS 很多优秀的收集思路,整个垃圾收集过程,可以分为如下 4 个步骤:
- 初始标记
- 并发标记
- 重新标记
- 筛选回收
G1 收集器的垃圾回收流程和 CMS 逻辑大致相同,主要的区别在最后一个阶段,G1 不会直接进行清除,而是会根据设置的停顿时间进行智能的筛选和局部的回收,采用“标记复制”算法来实现。
整个流程,可以用如下图来概括。
2.5.1、阶段一:初始标记
此阶段的工作内容与上文介绍的 CMS 收集器一样,会先把所有 GC Roots 直接引用的对象进行标记,同时会短暂的停止用户线程,因为不会对整个 GC Roots 的引用进行遍历,因此速度比较快。
2.5.2、阶段二:并发标记
此阶段的工作内容与上文介绍的 CMS 收集器也一样,找到所有与 GC Roots 关联的对象并进行深度遍历标记,会采用多线程的方式进行遍历标记,因为比较耗时,为了尽量不停顿用户线程,这个阶段 GC 线程会和用户线程同时运行,通过这样的方式减少用户线程的停顿时间。
2.5.3、阶段三:重新标记
此阶段的工作内容与上文介绍的 CMS 收集器也是一样,针对阶段二用户线程运行的过程中产生新的垃圾,采用多线程方式进行重新标记,为了避免这个过程再次产生新的垃圾对象,会短暂的停止用户线程,因为数量不会很多,因此速度比较快。
2.5.4、阶段四:筛选回收
筛选回收阶段的工作主要是把存活的对象复制到 Region 空闲区域,同时会根据 Collect Set 记录的可回收 Region 信息进行筛选,计算 Region 回收成本,接着根据用户设定的停顿时间值制定回收计划,最后根据回收计划筛选合适的 Region 区域进行垃圾回收。
从局部来看,G1 使用的是复制算法,将存活对象从一个 Region 区域复制到另一个 Region 空闲区域;但从整个堆来看,G1 使用的逻辑又相当于标记整理算法,每次垃圾收集时会把存活的对象整理到对应可用的 Region 区域,再把原来的 Region 区域标记为可回收区域并记录到 Collect Set 中,因此 G1 的每一次回收都可以看作是一次标记整理过程,两者都不会产生空间碎片问题。
2.5.5、小结
总结下来,收集器特点如下:
- 收集区域:整个堆内存
- 收集算法:复制算法
- 收集方式:多线程
- 优势:停顿时间可控,吞吐量高,不会产生空间碎片,不需要额外的收集器搭配
- 劣势:目前而言,相较于 CMS,G1 还不具备全方位、压倒性优势,G1 在收集过程中内存占用和执行负载都偏高;其次,在小内存应用上 CMS 的表现大概率会优于 G1,而 G1 在大内存应用上会比较有优势,6G 以上的内存可以考虑使用 G1 收集器
2.6、常用的收集器组合
最后我们对以上介绍的垃圾收集器进行一次汇总,同时介绍一下服务器端常用的组合模式,内容如下。
服务器组合 | 新生代收集器 | 老年代收集器 | 备注 |
---|---|---|---|
组合一 | Serial | Serial Old | Serial 是一个使用单线程采用复制算法的新生代收集器;Serial Old 是一个使用单线程采用标记整理算法的老年代收集器,GC 时会暂停所有应用线程,可以使用-XX:+UseSerialGC 选项来开启 |
组合二 | ParNew | Serial Old | ParNew 是一个使用多线程采用复制算法的新生代收集器,GC 时会暂停所有应用线程,可以使用-XX:+UseParNewGC 选项来开启 |
组合三 | Parallel Scavenge | Serial Old | Parallel Scavenge 是一个使用多线程采用复制算法的新生代收集器,GC 时会暂停所有应用线程,可以使用-XX:+UseParallelGC 选项来开启;需要注意的是,在jdk1.7及之前的版本中,这个参数默认采用 Serial Old 作为老年代收集器;在jdk1.8及之后的版本中,默认采用 Parallel Old 作为老年代收集器 |
组合四 | Parallel Scavenge | Parallel Old | Parallel Old是 Serial Old 的多线程版收集器,可以使用-XX:+UseParallelOldGC 选项来开启 |
组合五 | Serial | CMS + Serial Old | CMS 是一个使用多线程采用标记清楚算法的老年代收集器,可以实现 GC 线程和用户线程并发工作,不需要暂停所有用户线程;另外,可以将 Serial Old 收集器作为备选,当 CMS 进行 GC 失败时,会自动使用 Serial Old 进行 GC;可以使用-XX:+UseConcMarkSweepGC 选项来开启 |
组合六 | ParNew | CMS + Serial Old | ParNew 是除了 Serial 以外,唯一一个能搭配 CMS 的新生代收集器;可以使用-XX:+UseConcMarkSweepGC 开启,默认使用 ParNew 作为新生代收集器,也可以通过-XX:+UseParNewGC 强制指定 ParNew |
组合七 | G1 | G1 | G1 是一个新一代的垃圾收集器,摒弃了原来的物理分区,把整个 Java 堆分成若干个大小相等的独立区域(Region),针对局部区域使用多线程采用复制算法进行筛选回收,可以使用-XX:+UseG1GC 选项来开启 |
三、方法区回收
以上介绍的都是对象的回收过程,在之前的 JVM 内存结构的文章中我们介绍到,Java 应用程序运行时,除了堆空间会存在垃圾数据以外,方法区同样也存在。
虽然虚拟机规范中没有明确要求方法区一定要实现垃圾回收,主要原因在于这个区域的垃圾回收效率非常低,但是 HotSpot 虚拟机对方法区也会进行回收的,主要回收的是废弃常量和无用的类两部分。
如何判断一个常量是否为“废弃常量”呢?其实很简单,只要当前系统中没有任何一处引用该常量,就会被判定为废弃常量。
如何判断一个类是否为“无用的类”呢?条件非常苛刻,需要同时满足以下三点。
- 1.该类所有实例都已经被回收,也就是说 Java 堆中不存在该类的任何实例
- 2.该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法 - 3.加载该类的 ClassLoader 已经被回收,也就是说这个类的类加载器被卸载回收了
满足以上三个条件则表示这个类再也无用了,HotSpot 虚拟机会对此类进行回收。例如在大量使用反射、动态代理、CGLib 等 ByteCode 框架,并自定义 ClassLoader 创建的类,为了保证方法区不会溢出,虚拟机会在适当的情况下对无用的类进行回收。
在 JDK1.7 及以前的版本中,用永久代来作为方法区的实现,当永久代的空间不足时会触发 Full GC。
在 JDK1.8 及之后的版本中,用元空间来作为方法区的实现,元空间的内存空间默认使用的是操作系统的内存空间,它的垃圾回收不再由 Java 来控制,元空间的内存管理由元空间虚拟机来完成。
四、小结
本文主要围绕对象的回收判断方式,垃圾回收算法以及垃圾收集器,做了一次知识内容的整理和总结,如果有描述不当的地方,欢迎大家留言指出!
五、参考
1.https://zhuanlan.zhihu.com/p/267223891
2.https://www.cnblogs.com/xrq730/p/4836700.html
3.https://zhuanlan.zhihu.com/p/248709769
4.http://www.ityouknow.com/jvm/2017/09/28/jvm-overview.html
作者:潘志的技术笔记
出处:http://pzblog.cn/
版权归作者所有,转载请注明出处