与 C、C++等手动管理内存的编程语言不同,Java 程序在运行时会自动对内存中不再使用的对象进行检测,并回收这部分对象所占用的内存空间。

在 JDK 中,JVM 将堆(heap)内存分为了不同的区域块,每种区域会使用不同的垃圾收集器,每种垃圾收集器又会使用各自的垃圾收集算法。因此 JVM 的垃圾收集设计大致架构如下,从上至下越来越偏向于细节,本文使用自底向上的方式进行介绍

  1. 堆内存
  2. 堆内存分为多个区域
  3. 每个区域使用了不同的垃圾收集器
  4. 每种垃圾收集器使用了一个或多个垃圾收集算法

垃圾收集算法

检测垃圾的方式一般有引用计数法可达性分析两种,因为引用计数法无法检测到循环引用的垃圾对象,所以现在 JVM 中进行垃圾检测都是使用的可达性分析方法。

在检测到了垃圾对象之后,JVM 就需要对这些垃圾对象进行垃圾回收(英语:Garbage Collection,缩写为 GC),JVM 中使用到的GC 算法主要由如下三种

标记-清除算法(Mark-Sweep)

该算法分为标记和清除两个步骤,第一步标记是将所有活动的对象做上标记,第二部清除是将所有没有标记的对象进行回收。

标记-压缩算法(Mark-Compact)

标记压缩算法和标记清除算法基本上一致,只是标记清除算法过程只能单纯的把对象内存空间给释放出来,时间长了会造成内存碎片的问题。标记压缩算法在标记清除算法的基础上会对存活的对象进行移动,之后把这些对象向某一端进行移动来保证有完整的内存空闲空间。

标记压缩算法避免了内存碎片的问题,内存的利用率得到了提高,但是因为要移动对象所以它的 GC 效率要比标记清除算法要差。

复制算法(Copying)

复制算法将内存分为 FROM 和 TO 两块区域,对象在 From 区域内进行分配,之后对 FROM 区域进行 GC,存活的对象被全部复制 TO 区域;之后对象在 TO 区域进行分配,GC 发生在 TO 区域,GC 后存活的对象被复制到 FROM 区域,如此反复。

垃圾收集器

在 JVM 中存在着多种垃圾收集器,这些垃圾收集器使用了一种或多种垃圾收集算法,我们可以认为垃圾收集算法是垃圾收集器的理论基础。

下面是 JVM 中几种常见的垃圾回收器

垃圾回收器垃圾回收算法
Serial复制算法
ParNew复制算法
Serial Old标记-整理
CMS标记-清除
G1标记-整理+复制算法

经典的堆内存布局(与 G1 做区分)

堆是区别于栈、方法区等等的一大块内存空间,用于存放对象。JVM 将堆内存再进行分区,分为新生代(Young Generation)和老年代(Old Generation),在新生代进行的 GC 叫做 Young GC 或 Minor GC,在老年代进行的 GC 叫做 Old GC 或 Major GC,其中新生代的内存又可以分为伊甸(Eden)和存活区(Survivor),Survivor 分为两块,一般称为 From 和 To。典型的对象分配和 GC 流程如下

  1. JVM 将对象分配在新生代的 Eden 区域
  2. 当 Eden 中的对象内存占用到一定的阈值时,触发一次 Young GC
  3. JVM 将 Eden 中存活的对象复制到 Survivor 的 To 区域,此时 Eden 从逻辑上已经被清空
  4. 将 Survivor 的 From 和 To 区域的名字进行对调,即 To(此时包含上一次 GC 的存活对象)改为 From,From 改为 To
  5. JVM 继续将对象分配在 Eden
  6. Eden 中的对象到达一定的阈值之后触发一次 Young GC,该次 gc 发生在 Eden 和 Survivor 的 From(包含上一次 GC 的存活对象)这两个区中,JVM 将 Eden 和 From 区中的存活对象复制到 To 区
  7. 继续进行 From 和 To 区的名字对调
  8. JVM 继续向 Eden 分配对象,下次的 GC 会对 Eden 和 From 区中的对象进行,存活对象被复制到 To 区
  9. 如此往复,每次的 GC 存活对象都被保存到 Survivor 的 To,随后 To 重新改名为 From、From 也改名为 To,之后等待进行下一次的 GC

上面介绍的是新生代的 GC 过程,当 To 区域存活的对象到达一定的年纪之后(每次 GC 的存活对象都会增加一岁),JVM 就会将该对象移动到 Old Generation。老年代的内存分配相较于新生代要简单的多,只有一块区域。

以 JDK1.8 中流行的 -XX:+UseConcMarkSweepGC 选项为例,该选项会在新生代使用 ParNew 垃圾收集器,在老年代使用 CMS 垃圾收集器。

G1 垃圾收集器堆内存布局

与经典的堆内存不一样,G1 垃圾收集器会将堆内存分为一个个的 region 区域,使用多个 region 来替代以前连续的堆内存空间。内存分块这种思想与 Linux 中的内存分页十分类似,本质都是把内存打散以方便进行管理,用离散的内存来替代连续的内存。内存分块之后可以从根本上解决内存碎片的问题,并且内存管理起来更加的简单。

G1 的 GC 可以分为 Young Only Phase、Mixed gc Phase 和 Full gc Phase。

Young Only Phase

对象会不断地分配到 Eden 分区中,Eden 分区的数量会不断地增长,这个操作会一直进行下去直到 Eden 分区数量达到上限。

当 Eden 分区的数量达到了阈值之后,就会对所有的 Eden 分区进行 GC,之后存活的对象会移动到 Survivor 区中,如果没有 Survivor 区则会随意挑选一个空闲的 region 作为 Survivor 并保存存活对象。

同时如果 Survivor 分区中的某些对象达到了一定年纪之后,这些对象也会被复制到 Old 分区中。

Mixed gc Phase

Old 分区越来越大,当老年分区的占比达到了一定比例之后,就会触发针对年轻代和老年代的 GC,是为 Mixed gc。

在进行 Mixed gc 之前,先需要进行**并发标记周期 (Concurrent Marking Cycle)**,这个过程会分为 5 步

  1. 初始标记(Young Collection with Initial Mark),它伴随着一次普通的 Young GC 发生,然后对 Survivor 区进行标记,因为该区可能存在对老年代的引用,会触发 Stop The World
  2. 根区间扫描,因为先进行了一次 YGC,所以当前年轻代只有 Survivor 区有存活对象,它被称为根引用区(root region)。扫描 Survivor 到老年代的引用,该阶段必须在下一次 Young GC 发生前结束
  3. 并发标记(Concurrent Marking),寻找整个堆的存活对象,该阶段可以被 Young GC 中断
  4. 重新标记(Remark),完成最后的存活对象标记。使用了比 CMS 收集器更加高效的 snapshot-at-the-beginning (SATB) 算法,会触发 Stop The World
  5. 清除(Clean),清理阶段真正回收的内存很少

到这里,G1 的一个并发周期就算结束了,其实就是主要完成了垃圾定位的工作,定位出了哪些分区是垃圾最多的。因为整堆一般比较大,所以这个周期应该会比较长,中间可能会被多次 STW 的 Young GC 打断。

等到并发标记周期完成之后,就会进入 Mixed gc Phase 了,混合垃圾收集周期既会回收新生代的垃圾,也会回收老年代的垃圾。

Full gc Phase

当以下两个现象同时发生时就会触发 Full GC

  1. 空闲分区不足或巨型对象无法在老年代找到连续的分区,此时 JVM 报错 to-space exhausted。
  2. 在现象 1 发生时,G1 会尝试增加堆的使用量,如果增加失败则会触发 Full gc

Full gc 时单个线程会对整个堆的所有代中所有分区做标记、清除以及压缩动作,非常昂贵。

可以通过对 JVM 设置如下的 Xlog 来打印 GC 相关的日志以方便对 JVM 的 GC 过程进行深入的了解:

-Xlog:gc*:file=logs/gc.log:t,tags,level:filecount=5,filesize=100m

G1 在 Elasticsearch 中的使用

Elasticsearch 对 JDK 8u40 之前的 Java 版本是 不推荐使用 G1 垃圾收集器的,如果检测到 错误的 Java 版本和 GC 配置,Elasticsearch 会启动失败。但是在 Elasticsearch 7.5.2 自带的版本为 13.0.1 的 bundled JDK 中,G1 已经成为了默认的垃圾收集器,G1 已经通过了 ES 的测试验证 并且 ES 团队也给出了一些关于 Java 堆 (heap) 内存管理的 最佳实践

参考

咱们从头到尾说一次 Java 垃圾回收 - InfoQ
JVM 中的垃圾回收策略
搞懂 G1 垃圾收集器 - 博客园
G1GC 概念与性能调优 - OPPO 互联网技术
G1 垃圾收集器介绍 - Javadoop
Universal GC Log Analyzer
Java Hotspot G1 GC 的一些关键技术 - 美团技术团队
新一代垃圾回收器 ZGC 的探索与实践 - 美团技术团队

Getting Started with the G1 Garbage Collector
Go 垃圾回收(三)——三色标记法是什么鬼?
V8 增量 GC 之三色标记
Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide
jdk11 下 g1 收集器使用