JVM系列三(垃圾收集器).

一、概述

1. 哪些内存需要回收

上篇文章 我们介绍了 Java 内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。

而方法区和 Java 堆是线程共享的,我们只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。

2. 回收方法区

方法区的垃圾收集主要回收两部分内容:废弃常量和无用的类。

“废弃常量”指的是当前系统中没有任何一个对象引用指向该常量。

“无用的类”需要同时满足下面三个条件才有可能被虚拟机回收,至于最终是否回收还由虚拟机参数:-Xnoclassgc 控制。

  • 该类的所有实例都已被回收,也就是 Java 堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 Class 对象没有被任何地方被引用,无法在任何地方通过反射访问该类的接口。

二、经典垃圾回收器

首先开始之前先看下 HotSpot 虚拟机所包含的收集器:

图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器。

1. Serial 收集器

新生代收集器,复制算法收集,Serial 收集器是最基本、发展历史最悠久的收集器。它是一个单线程的收集器,只会使用一个 CPU 或一条收集线程去完成垃圾收集工作,它在垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

优点:简单高效;虚拟机 Client 模式下表现优异(Client 模式下内存较小、CPU较少,能减少许多线程交互的开销)。
缺点:回收工作需要 Stop The World ;单线程;不适用虚拟机 Server 模式(Server 模式下内存较大、CPU较多,导致回收工作停顿时间过长)。

2. ParNew 收集器

新生代收集器,复制算法收集,ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行回收外,其余行为包括控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与 Serial 收集器完全一样。

优点:多线程工作;可以与 CMS 收集器搭配工作;虚拟机 Server 模式下表现优异。
缺点:回收工作需要 Stop The World 。

3. Parallel Scavenge 收集器

新生代收集器,复制算法收集,多线程工作,Parallel Scavenge 收集器的关注点在于达到一个可控制的吞吐量(其他收集器的关注点是缩短垃圾收集时用户线程的停顿时间),停顿时间越短越适合需要与用户交互的程序;而高吞吐量则可以高效率的利用 CPU 时间,尽快完成程序的运行任务。

GC 自适应调节策略是 Parallel Scavenge 收集器和 ParNew 收集器的一个重要区别。它变现为:只需要把基本的内存数据设置好(如 -Xmx 设置最大堆),然后使用 MaxGCPauseMillis 参数(更关注最大停顿时间)或 GCRatio(更关注吞吐量)给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机来完成了。

优点:多线程工作;注重系统吞吐量和CPU资源;自适应调节策略。
缺点:回收工作需要 Stop The World ;可选的老年代收集器过少,无法与 CMS 收集器配合工作,在 JDK1.5 之前只能和 Serial Old 收集器配合工作。

tips:

  • 吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
  • 自适应调节策略使用 -XX:+UseAdptiveSizePolicy 参数打开。
  • 与吞吐量关系密切,故也称为“吞吐量优先”收集器。

4. Serial Old 收集器

老年代收集器,标记-整理算法,单线程,Serial Old 收集器是 Serial 收集器的老年代版本。

优点:虚拟机 Client 模式下表现尚可(Client 模式下内存较小、CPU较少,能减少许多线程交互的开销);CMS 收集器的后备预案(在并发收集Concurent Mode Failure时使用)。
缺点:回收工作需要 Stop The World ;单线程。

5. Parallel Old 收集器

老年代收集器,标记-整理算法,多线程,Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,在 JDK1.6 后开始提供。

优点:搭配 Parallel Scavenge 收集器使用,关注系统吞吐量以及CPU资源。
缺点:回收工作需要 Stop The World ;可搭配的新生代收集器仅有 Parallel Scavenge 收集器而已。

6. CMS 收集器

老年代收集器,标记-清除算法,多线程,CMS(Concurrent Mark Sweep)收集器是一种以获得最短回收停顿时间为目标的收集器,是真正意义上与用户线程并发运行的收集器,因此,使用 CMS 收集器能给用户带来良好的体验。

优点:

  1. 并发收集;低停顿。
  2. 对于大概 4GB 到 6GB 以下的堆内存,CMS 一般处理的比较好。
    缺点:
  3. CMS 收集器对 CPU 资源敏感,在并发标记/清理 的时候,虽然不会导致用户线程停顿,但标记/清理工作是要占用一部分 CPU 资源的,这无疑会降低吞吐量。(CMS 默认启动的回收线程数是 (CPU 数量 + 3)/ 4)
  4. CMS 收集器无法处理浮动垃圾(Floating Garbage),可能出现 “Concurent Mode Failure” 失败而导致另一次 Full GC 的产生(使用 Serial Old 收集器)。浮动垃圾指的是并发清理阶段,用户线程并发运行产生的垃圾,当这些浮动垃圾的内存超过了CMS 运行期间预留的内存,就会导致 “Concurent Mode Failure” 失败。
  5. CMS 收集器使用的标记-清除算法会有大量的内存碎片出现,将会给大对象分配带来很多麻烦。

CMS 收集器的一些好文章:

7. G1 收集器

分区(Region)收集器,标记-整理算法和复制算法,多线程,是一款面向服务端应用的垃圾收集器,它的目标也是获得最短停顿时间 —— 消耗在垃圾收集上的时间大概率不超过 N 毫秒。

G1(Garbage-First)收集器在 JDK 7u4 版本发布,它的期望是未来可以替换 CMS 收集器。终于在 JDK9 中 G1 收集器成为了默认的垃圾收集器,而 CMS 则成为了不推荐使用的收集器。

虽然 G1 收集器仍是遵循分代理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1 不再坚持固定大小以及固定数量的分代区域划分,而是把连续的 Java 堆划分为多个大小相等的独立区域(Region),每一个 Region 都可以根据需要,扮演新生代的 Eden 空间、Survivor空间,或者老年代空间。G1 可以面对任何 Region 来组成回收集(Collection Set,一般简称为 CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大。从而优先处理回收价值收益最大的那些 Region,这也就是 “Garbage First”名字的由来。

优点:

  1. 并行和并发,缩短 Stop The World 停顿的时间。
  2. 标记-整理算法、复制算法不会出现类似 CMS 的内存碎片问题。
  3. 可预测的停顿时间模型(-XX:G1HeapRegionSize 设置,默认是 200 ms),能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不超过 N 毫秒。

推荐场景:

G1的首要目的是为那些需要大容量内存和较小 GC 延迟的应用程序提供解决方案。这通常是指那些堆大小设置在 6GB 以上,确定的、可以预测的暂停时间在 0.5 秒以内的应用程序。

如果应用程序符合以下一项或者多项特征,那么从 CMS 或者 ParallelOld 收集器切换到 G1 可能更合适。

  • 活动对象占据了超过 50% 的 Java 堆空间。
  • 对象分配率或者提升率波动明显。
  • 不希望有长时间的垃圾收集暂停时间(超过0.5秒或1秒)。

三、低延迟垃圾收集器

从 G1 开始,最先进的垃圾收集器的设计导向都不约而同地变为追求能够应付应用的内存分配速率,而不追求一次把整个 Java 堆全部清理干净。这样,应用在分配,同时收集器在收集,只要收集的速度能够跟得上对象分配的速度,那一切就能运作的很完美。

衡量垃圾收集器的三项最重要的指标是:内存占用(Footprint)、吞吐量(Throughput)和延迟(Latency),三者共同构成了一个“不可能三角”。随着计算机硬件的发展,“内存占用”和“吞吐量”的缺陷可以由硬件来弥补,因此“延迟”成为了垃圾收集器最被重视的性能指标。

ZGC(JDK11)、Shenandoah(OpenJDK12) 收集器,几乎整个工作过程全部都是并发的,只有初始标记、最终标记这些阶段有短暂的停顿,这部分停顿的时间基本上是固定的,与堆的容量、堆中对象的数量没有正比例关系(可以在任意可管理的堆容量下,实现垃圾收集的停顿不超过10ms),这两款目前仍处于实验状态的收集器,被官方命名为“低延迟垃圾收集器”。

1. Shenandoah 收集器

Shenandoah 收集器,标记-整理算法和复制算法,多线程, 与 G1 类似也使用 Region 的堆内存布局,也使用 Humongous Region 存放大对象,默认的回收策略也同样是优先回收价值最大的 Region。

Shenandoah 的回收阶段可以与用户线程并发,这是 Shenandoah 的核心功能,而 G1 收集器无法做到。

Shenandoah 默认不使用分代收集算法,不区分新生代和老年代。

Shenandoah 摒弃了在 G1 中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”(Connection Matrix)的全局数据结构来记录跨 Region 的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题的发生概率。

Shenandoah 收集阶段大抵可以理解为三个并发阶段(并发标记、并发回收、并发引用更新),其中并发回收是最核心的阶段,基于标记-整理算法来并发回收是很复杂的,其困难点是在移动对象的同时,用户线程仍然可能不停对被移动的对象进行读写访问,很难一瞬间全部改变过来。 Shenandoah 采取的方式是读屏障和“Brooks Pointers”的转发指针,包括后面的并发引用更新,也是为了解决这个事宜。

2. ZGC 收集器

ZGC 收集器,标记-整理算法和复制算法,多线程,基于 Region 内存布局,不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法,以低延迟为首要目标。

ZGC 收集器中 Region 具有动态性 —— 动态创建和销毁以及动态的区域容量大小。在 x64 硬件平台上,ZGC 的 Region 分为大、中、小三类容量。

ZGC 每次回收都会扫描所有的 Region,用范围更大的扫描成本换取省去 G1 中记忆集的维护成本。

四、另类的收集器 —— Epsilon

Epsilon 收集器出现在 JDK11 中,这是一款以不能够进行垃圾收集为“卖点”的垃圾收集器。

近年来大型系统从传统单体应用向微服务化、无服务化方向发展的趋势已越发明显,传统 Java 有着内存占用较大,在容器中启动时间长,即时编译需要缓慢优化等特点,这对大型应用来说并不是什么太大的问题,但对短时间、小规模的服务形式就有诸多不适。为了应对新的技术潮流,最近几个版本的 JDK 逐渐加入了提前编译、面向应用的类数据共享等支持。Epsilon 收集器也有着类似的目标,如果应用只要运行数分钟甚至数秒,只要Java虚拟机能正确分配内存,在堆耗尽之前就会退出,那显然运行负载极小、没有任何回收行为的 Epsilon 便是恰当的选择。


参考链接:
  1. 《深入理解 JVM 虚拟机》

  2. G1垃圾收集器介绍

  3. jvm垃圾收集器(终结篇)

posted @ 2019-12-17 21:05  JMCui  阅读(526)  评论(0编辑  收藏  举报