【JVM | 第五篇】—— 深入理解垃圾回收

目录

前言

一、垃圾回收的基本原理

[1.1 什么是垃圾](#1.1 什么是垃圾)

[1.2 如何判断对象是否存活](#1.2 如何判断对象是否存活)

方法一:可达性分析算法

方法二:引用计数法(已淘汰)

二、经典垃圾回收算法

[2.1 标记 - 清除算法(Mark-Sweep)](#2.1 标记 - 清除算法(Mark-Sweep))

[2.2 复制算法(Copying)](#2.2 复制算法(Copying))

[2.3 标记 - 整理算法(Mark-Compact)](#2.3 标记 - 整理算法(Mark-Compact))

[2.4 分代收集算法(Generational Collection)](#2.4 分代收集算法(Generational Collection))

三、垃圾回收器概览

[四、CMS 垃圾回收器:曾经的低延迟之王](#四、CMS 垃圾回收器:曾经的低延迟之王)

[4.1 CMS 的设计目标](#4.1 CMS 的设计目标)

[4.2 CMS 的工作过程](#4.2 CMS 的工作过程)

[4.3 CMS 的优点](#4.3 CMS 的优点)

[4.4 CMS 的致命缺点(为什么被淘汰)](#4.4 CMS 的致命缺点(为什么被淘汰))

[五、G1 垃圾回收器](#五、G1 垃圾回收器)

[5.1 G1 的设计理念](#5.1 G1 的设计理念)

[5.2 G1 的内存划分](#5.2 G1 的内存划分)

[5.3 G1 的工作过程](#5.3 G1 的工作过程)

[5.3.1. 年轻代回收(Young GC)](#5.3.1. 年轻代回收(Young GC))

[5.3.2. 并发标记周期](#5.3.2. 并发标记周期)

[5.3.3. 混合回收(Mixed GC)](#5.3.3. 混合回收(Mixed GC))

[5.3.4. Full GC](#5.3.4. Full GC)

[5.4 G1 的关键特性](#5.4 G1 的关键特性)

[5.5 G1 为什么能取代 CMS](#5.5 G1 为什么能取代 CMS)

[六、Java GC 机制详解](#六、Java GC 机制详解)

[6.1 几种 GC 的区别](#6.1 几种 GC 的区别)

[6.2 GC 的触发时机](#6.2 GC 的触发时机)

七、Stop-The-World(STW)

[7.1 什么是 STW](#7.1 什么是 STW)

[7.2 为什么需要 STW](#7.2 为什么需要 STW)

[7.3 什么时候会发生 STW](#7.3 什么时候会发生 STW)

[7.4 如何减少 STW](#7.4 如何减少 STW)

前言

本文将从最基础的垃圾回收原理讲起,深入剖析经典的垃圾回收算法,详细对比 CMS 和 G1 两大主流回收器,解释 CMS 被淘汰的根本原因,最后重点讲解现在最常用的 G1 垃圾回收器的设计理念和工作原理。读完这篇文章,你将彻底理解 Java GC 的核心机制,轻松应对面试中的 GC 问题,同时也能在实际工作中更好地排查和解决 JVM 性能问题。

一、垃圾回收的基本原理

1.1 什么是垃圾

在 JVM 中,垃圾指的是堆内存中已经不再被任何存活线程引用的对象 。如果这些垃圾对象不及时清理,它们会一直占用堆内存,直到内存耗尽,最终抛出**OutOfMemoryError**异常。

1.2 如何判断对象是否存活

垃圾回收的第一步是判断哪些对象是垃圾,哪些对象还活着。JVM 主要使用可达性分析算法来判断对象的存活状态。

方法一:可达性分析算法

通过一系列称为 "GC Roots" 的根对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,就证明这个对象是不可用的,可以被回收。

可以作为 GC Roots 的对象包括:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈中 JNI(本地方法)引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 同步锁(synchronized 关键字)持有的对象

这里涉及到Java中内存分配的相关知识,可以看我这篇【JVM | 第四篇】------ JVM 内存分配-CSDN博客

方法二:引用计数法(已淘汰)

给每个对象添加一个引用计数器,每当有一个地方引用它时,计数器加 1;当引用失效时,计数器减 1。任何时刻计数器为 0 的对象就是不可能再被使用的。

缺点: 无法解决循环引用的问题。例如:A 引用 B,B 引用 A,但它们都没有被其他对象引用,这时候它们的引用计数器都不为 0,但实际上它们已经是垃圾了。

二、经典垃圾回收算法

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

原理: 分为两个阶段:

  1. 标记阶段: 遍历所有对象,标记出所有需要回收的对象
  2. 清除阶段: 遍历堆内存,回收所有被标记的对象

优点: 实现简单,不需要移动对象

缺点:

  • 会产生大量的内存碎片,导致后续分配大对象时无法找到足够的连续内存,不得不提前触发 GC
  • 清除效率较低,需要遍历整个堆内存

2.2 复制算法(Copying)

原理: 将可用内存划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还活着的对象复制到另一块上面,然后把已经使用过的这一块内存一次性全部清理掉。

优点:

  • 没有内存碎片
  • 分配内存时只需要移动指针,效率极高
  • 只需要复制存活对象,清除效率高

缺点:

  • 内存利用率低,只有一半的内存可用
  • 如果存活对象很多,复制的开销会很大

应用场景: 新生代的垃圾回收,因为新生代中的对象大部分都是朝生夕死的,存活对象很少,复制的开销很小。

2.3 标记 - 整理算法(Mark-Compact)

原理: 分为三个阶段:

  1. 标记阶段: 与标记 - 清除算法相同,标记出所有需要回收的对象
  2. 整理阶段: 将所有存活的对象向内存的一端移动
  3. 清除阶段: 直接清理掉边界以外的所有内存

优点:

  • 没有内存碎片
  • 内存利用率高

缺点:

  • 需要移动对象,效率较低
  • 会产生较长的 STW时间(STW会在下文介绍)

应用场景: 老年代的垃圾回收,因为老年代中的对象存活时间长,存活对象多,不适合使用复制算法。

2.4 分代收集算法(Generational Collection)

原理: 根据对象的存活周期将堆内存划分为不同的区域,然后根据不同区域的特点采用不同的垃圾回收算法。

现代 JVM 都采用分代收集算法,将堆内存划分为:

  • 新生代(Young Generation): 存放刚创建的对象,大部分对象很快就会死亡。采用复制算法
  • 老年代(Old Generation): 存放存活时间较长的对象。采用标记 - 清除标记 - 整理算法

可以将其看作是对于前三种算法的混合使用。

三、垃圾回收器概览

垃圾回收算法是内存回收的方法论,而垃圾回收器是内存回收的具体实现。不同的垃圾回收器适用于不同的场景,没有最好的垃圾回收器,只有最合适的垃圾回收器。

表格

垃圾回收器 适用代 特点 适用场景
Serial 新生代 单线程,复制算法,STW 客户端模式,单核 CPU
Parallel Scavenge 新生代 多线程,复制算法,STW,吞吐量优先 后台计算任务,对响应时间要求不高
Serial Old 老年代 单线程,标记 - 整理算法,STW 客户端模式,单核 CPU
Parallel Old 老年代 多线程,标记 - 整理算法,STW,吞吐量优先 与 Parallel Scavenge 配合使用
CMS 老年代 多线程,标记 - 清除算法,并发回收,低延迟 互联网应用,对响应时间要求高
G1 全堆 多线程,标记 - 整理 + 复制算法,并发回收,可预测停顿 通用场景,现在的默认回收器
ZGC 全堆 并发回收,低延迟(亚毫秒级) 对响应时间要求极高的应用
Shenandoah 全堆 并发回收,低延迟 对响应时间要求极高的应用

这里主要了解CMS,G1就行。

四、CMS 垃圾回收器:曾经的低延迟之王

4.1 CMS 的设计目标

CMS(Concurrent Mark Sweep)是一款以获取最短回收停顿时间为目标的垃圾回收器。它非常适合那些重视服务响应速度的应用,比如互联网网站、电商系统等。

4.2 CMS 的工作过程

CMS 采用标记 - 清除算法,整个回收过程分为四个阶段:

  1. 初始标记: 标记 GC Roots 能直接关联到的对象,速度很快,会发生 STW
  2. 并发标记: 从 GC Roots 的直接关联对象开始遍历整个对象图,这个过程耗时较长,但可以与用户线程并发执行
  3. 重新标记: 修正并发标记期间因用户线程继续运行而导致标记产生变动的那一部分对象的标记记录,会发生 STW,停顿时间比初始标记稍长,但远短于并发标记。
  4. 并发清除: 清除所有被标记的垃圾对象,可以与用户线程并发执行

4.3 CMS 的优点

  • 低延迟: 最主要的优点,大部分回收过程都可以与用户线程并发执行,停顿时间很短。
  • 响应速度快: 适合对响应时间要求高的应用。

4.4 CMS 的致命缺点(为什么被淘汰)

CMS 虽然实现了低延迟,但它也有很多无法解决的问题,最终被 G1 所取代:

  1. CPU 资源敏感: 并发阶段会占用大量的 CPU 资源,导致应用程序的吞吐量下降。在 CPU 核心数较少的机器上,这个问题尤为明显。

  2. 无法处理浮动垃圾: 在并发清除阶段,用户线程还在运行,会产生新的垃圾对象,这些垃圾对象只能等到下一次 GC 才能被回收。为了应对这种情况,CMS 不能等到老年代完全填满时才进行回收,必须预留一部分空间给并发收集期间的新对象。如果预留空间不足,就会发生 "Concurrent Mode Failure",此时 JVM 会临时启用 Serial Old 回收器来进行老年代的回收,导致长时间的 STW。

  3. 内存碎片问题: CMS 使用标记 - 清除算法,会产生大量的内存碎片。当需要分配大对象时,无法找到足够的连续内存,不得不提前触发 Full GC。为了解决这个问题,CMS 提供了-XX:+UseCMSCompactAtFullCollection参数,在 Full GC 时进行内存整理,但这会增加 STW 时间。

  4. 停顿时间不可预测: CMS 虽然大部分过程是并发的,但初始标记和重新标记阶段仍然需要 STW,而且重新标记阶段的停顿时间可能会很长,特别是在堆内存很大的情况下。

  5. 不适合大堆: 随着堆内存越来越大,CMS 的缺点变得越来越明显。并发标记阶段的时间会随着堆内存的增大而线性增加,重新标记阶段的停顿时间也会越来越长。

五、G1 垃圾回收器

5.1 G1 的设计理念

G1(Garbage-First)垃圾回收器是在 JDK 7 中正式推出的,在 JDK 9 中成为了默认的垃圾回收器,取代了 CMS。G1 的设计目标是在高吞吐量的同时,实现可预测的低停顿时间

G1 打破了传统的分代收集的概念,它将整个堆内存划分为多个大小相等的独立区域(Region),每个 Region 都可以根据需要扮演新生代的 Eden 区、Survivor 区或者老年代的空间。G1 会跟踪每个 Region 中垃圾的价值(即回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的停顿时间,优先回收价值最大的 Region。这就是 "Garbage-First" 名称的由来。

5.2 G1 的内存划分

G1 将堆内存划分为大约 2048 个大小相等的 Region,每个 Region 的大小在 1MB 到 32MB 之间,具体大小由 JVM 根据堆的总大小自动计算。

  • Eden Region: 存放新创建的对象
  • Survivor Region: 存放经过一次 GC 后仍然存活的对象
  • Old Region: 存放存活时间较长的对象
  • Humongous Region: 专门用来存放大对象(大小超过 Region 大小的 50% 的对象)

5.3 G1 的工作过程

G1 的回收过程主要分为以下几个阶段:

5.3.1. 年轻代回收(Young GC)

当 Eden 区满了之后,G1 会触发一次 Young GC。G1 会暂停所有用户线程(STW),将 Eden 区和 Survivor 区中存活的对象复制到新的 Survivor 区或者晋升到老年代的 Region 中。

Young GC 的停顿时间很短,因为只处理新生代的 Region,而且 G1 会根据之前的回收经验,预测这次回收需要的时间,从而控制停顿时间在用户设定的目标范围内。

5.3.2. 并发标记周期

当整个堆的使用率达到一定阈值(默认是 45%)时,G1 会触发并发标记周期。这个过程分为多个阶段:

  • 初始标记: 标记 GC Roots 能直接关联到的对象,会发生 STW,但时间很短。
  • 根区域扫描: 扫描 Survivor 区中引用到老年代的对象,可以与用户线程并发执行
  • 并发标记: 遍历整个对象图,标记出所有存活的对象,可以与用户线程并发执行
  • 重新标记: 修正并发标记期间因用户线程继续运行而导致标记产生变动的那一部分对象的标记记录,会发生 STW,但时间比 CMS 的重新标记短得多。
  • 清理阶段: 统计各个 Region 的存活对象数量和垃圾价值,更新优先列表,同时回收完全没有存活对象的 Region,会发生短暂的 STW

5.3.3. 混合回收(Mixed GC)

并发标记周期完成后,G1 知道了哪些 Region 中垃圾最多。接下来 G1 会触发多次 Mixed GC,回收所有的年轻代 Region 和一部分垃圾价值最高的老年代 Region。

Mixed GC 会暂停所有用户线程(STW),但 G1 会根据用户设定的停顿时间目标,每次只回收一部分 Region,从而控制停顿时间。

5.3.4. Full GC

只有在特殊情况下,G1 才会触发 Full GC:

  • 并发标记周期还没完成,堆内存就已经满了
  • 无法找到足够的空间来晋升对象
  • 无法分配大对象

G1 的 Full GC 是单线程的(JDK 10 之前),会产生很长的 STW 时间,所以在实际应用中应该尽量避免 Full GC 的发生。

5.4 G1 的关键特性

  1. 可预测的停顿时间: 这是 G1 最大的优势。G1 会跟踪每个 Region 的回收时间和回收价值,在后台维护一个优先列表,每次根据用户设定的停顿时间目标(-XX:MaxGCPauseMillis,默认 200ms),优先回收价值最大的 Region。

  2. 无内存碎片: G1 整体上采用标记 - 整理算法,局部上采用复制算法,不会产生内存碎片。这对于分配大对象非常有利,不会因为找不到连续内存而提前触发 Full GC。

  3. 并行与并发: G1 充分利用了多核 CPU 的优势,在多个阶段都采用了多线程并行执行,同时在并发标记和并发清理阶段可以与用户线程并发执行。

  4. 分代收集: G1 仍然保留了分代的概念,但它不需要将堆内存划分为连续的新生代和老年代,而是将整个堆划分为多个 Region,每个 Region 都可以动态地扮演不同的代。

  5. 大对象处理: G1 专门为大对象设计了 Humongous Region,避免了大对象在新生代和老年代之间来回复制,提高了大对象的分配和回收效率。

5.5 G1 为什么能取代 CMS

  1. 停顿时间更短且可预测: G1 的停顿时间比 CMS 更短,而且可以通过参数精确控制。CMS 的重新标记阶段停顿时间可能会很长,特别是在大堆的情况下。

  2. 没有内存碎片: G1 不会产生内存碎片,而 CMS 会产生大量的内存碎片,导致提前触发 Full GC。

  3. 适合大堆: G1 的性能不会随着堆内存的增大而显著下降,它可以很好地支持几十 GB 甚至上百 GB 的大堆。

  4. 吞吐量更高: G1 在实现低延迟的同时,也保持了很高的吞吐量。

  5. 更完善的功能: G1 解决了 CMS 的很多问题,比如浮动垃圾、Concurrent Mode Failure 等。

六、Java GC 机制详解

6.1 几种 GC 的区别

GC 类型 发生区域 触发条件 特点
Young GC(Minor GC) 新生代 Eden 区满了 频率高,停顿时间短
Old GC(Major GC) 老年代 老年代空间不足 频率低,停顿时间长
Full GC 整个堆 老年代空间不足、方法区空间不足、System.gc () 调用、Concurrent Mode Failure 等 停顿时间很长,应该尽量避免
Mixed GC 年轻代 + 部分老年代 并发标记周期完成后 G1 特有,停顿时间可控

6.2 GC 的触发时机

  1. Young GC 的触发时机: 当 Eden 区没有足够的空间来分配新对象时,就会触发 Young GC。

  2. Old GC 的触发时机:

    • 大对象直接进入老年代,当老年代没有足够的空间来容纳大对象时
    • 对象在新生代中经过多次 GC 后仍然存活,晋升到老年代,当老年代没有足够的空间时
    • Young GC 时,Survivor 区没有足够的空间来容纳存活的对象,这些对象会直接晋升到老年代,当老年代没有足够的空间时
  3. Full GC 的触发时机:

    • 老年代空间不足
    • 方法区(元空间)空间不足
    • 显式调用System.gc()方法(不推荐)
    • CMS 发生 Concurrent Mode Failure
    • G1 并发标记周期还没完成,堆内存就已经满了

七、Stop-The-World(STW)

7.1 什么是 STW

Stop-The-World(STW)指的是在垃圾回收过程中,JVM 会暂停所有正在执行的用户线程,直到垃圾回收完成。

这就是为什么有时候你的 Java 程序会突然卡顿一下,特别是在进行垃圾回收的时候。

7.2 为什么需要 STW

STW 是必要的,因为在垃圾回收过程中,需要确保对象图的一致性。如果在标记对象的同时,用户线程还在修改对象的引用关系,那么标记的结果就会不准确,可能会导致错误地回收掉还在使用的对象,或者漏掉一些垃圾对象。

7.3 什么时候会发生 STW

所有的垃圾回收器都会发生 STW,只是停顿时间的长短不同而已。

  • Serial/Parallel 回收器: 整个回收过程都会发生 STW
  • CMS 回收器: 初始标记和重新标记阶段会发生 STW
  • G1 回收器: 初始标记、重新标记、清理、Young GC 和 Mixed GC 阶段都会发生 STW,但停顿时间很短且可预测

7.4 如何减少 STW

  1. 选择合适的垃圾回收器: 对于对响应时间要求高的应用,应该选择 G1、ZGC 或 Shenandoah 等低延迟回收器。
  2. 合理设置堆内存大小: 堆内存太小会导致频繁 GC,堆内存太大则会导致单次 GC 的停顿时间过长。
  3. 优化代码: 减少大对象的创建,避免内存泄漏,及时释放不再使用的对象引用。
  4. 调整 GC 参数: 根据应用的特点调整 GC 参数,比如设置合适的停顿时间目标、新生代大小等。
相关推荐
IT龟苓膏2 小时前
Java 集合进阶:ConcurrentHashMap、HashSet、LinkedHashMap、TreeMap 和 fail-fast 一篇讲清
java·开发语言·jvm
J-Tony113 小时前
【JVM】双亲委派
jvm
ourenjiang3 小时前
【测试框架Junit】强制终止JVM进程
jvm·junit
光影6273 小时前
Python接口自动化测试----Requests库基础入门
开发语言·python·测试工具·pycharm·自动化
Full Stack Developme5 小时前
G1回收器的工作机制
java·jvm
填满你的记忆5 小时前
JVM 面试题 Top40
jvm·面试题
故渊at5 小时前
第二板块:Android 四大组件标准化学理 | 第十篇:ContentProvider 数据共享与 SQLite 引擎
android·jvm·数据库·sqlite·contentprovider
骄马之死5 小时前
JVM 核心知识
java·jvm
Java面试题总结5 小时前
采集网关的离线缓存与断点续传——当网络不可靠时,数据一条都不能丢
网络·jvm·缓存