深入探索 Java 垃圾回收算法:从原理到调优实践

373 阅读20分钟

你是否遇到过 Java 应用突然卡顿,或深夜收到系统内存溢出的告警?这些问题往往与 Java 垃圾回收机制息息相关。作为 Java 开发者,理解垃圾回收不仅能让你深入把握 JVM 底层运行原理,还能帮你解决性能瓶颈,提高系统稳定性。好的 GC 配置可能是系统性能优化的关键一步。

什么是垃圾回收?

Java 垃圾回收(Garbage Collection,简称 GC)是 JVM 自动管理内存的机制,负责识别和回收不再使用的对象所占用的内存空间。

在 Java 中,程序员不需要像 C/C++那样手动释放内存,JVM 会自动判断哪些对象是"垃圾",并在适当的时机回收它们。

JVM 主要通过可达性分析来判断对象是否存活。从一系列 GC Roots 出发,通过引用关系搜索,能被搜索到的对象标记为存活,其余的则是垃圾。

GC Roots 主要包括:

  • 虚拟机栈中引用的对象(线程栈帧中的本地变量表)
  • 方法区中静态属性引用的对象(静态变量)
  • 方法区中常量引用的对象(如字符串常量池)
  • 本地方法栈中 JNI 引用的对象
  • JVM 内部引用(如类加载器、异常对象、同步锁等)

除了强引用外,Java 还提供了软引用、弱引用和虚引用:

  • 软引用:内存不足时才会被回收,适合缓存场景
  • 弱引用:下一次 GC 时无论内存是否充足都会回收
  • 虚引用:不影响对象生命周期,用于跟踪对象被回收的状态
graph TD
    GCRoots[GC Roots] --> A[对象A]
    A --> B[对象B]
    A --> C[对象C]
    B --> D[对象D]
    E[对象E] --> F[对象F]
    style E fill:#f99,stroke:#333
    style F fill:#f99,stroke:#333

上图中,对象 E 和 F 无法从 GC Roots 到达,因此会被标记为垃圾。

Java 中的垃圾回收算法与回收器

以下内容既包括基础算法(如标记-清除、复制等基础理论),也包括分代策略和现代垃圾回收器实现(G1、ZGC 等)。后者是前者的工程化组合与优化,通过不同算法的组合和改进来满足不同场景的需求。

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

标记-清除是最基础的垃圾回收算法,分为两个阶段:

  1. 标记阶段:遍历所有 GC Roots 可达的对象,标记为存活对象
  2. 清除阶段:遍历整个堆,回收未被标记的对象
graph LR
    subgraph 标记前
    A1[对象A] --- B1[对象B]
    C1[对象C]
    D1[对象D]
    end

    subgraph 标记后
    A2["对象A ✓"] --- B2["对象B ✓"]
    C2[对象C]
    D2["对象D ✓"]
    end

    subgraph 清除后
    A3[对象A] --- B3[对象B]
    空白C[" "]
    D3[对象D]
    end

优点

  • 实现简单,基本思路清晰

缺点

  • 效率低:标记和清除过程效率都不高
  • 空间问题:清除后会产生大量不连续的内存碎片

案例分析: 当应用创建大量短生命周期的小对象时,使用标记-清除算法会导致严重的内存碎片问题。例如,Web 应用在处理 HTTP 请求时创建的临时对象:

public void processRequest(HttpRequest request) {
    String data = request.getParameter("data"); // 临时字符串
    JsonObject json = new JsonParser().parse(data); // 临时JSON对象
    // 处理逻辑
    // 方法结束后,data和json都会变成垃圾
}

若频繁创建/销毁这类小对象,碎片会越来越多,最终可能导致OutOfMemoryError(需要分配较大对象时找不到足够的连续空间,即使总空闲内存充足)。

2. 标记-整理算法(Mark-Compact)

标记-整理算法是对标记-清除算法的改进,解决了内存碎片的问题。

  1. 标记阶段:与标记-清除算法相同
  2. 整理阶段:将所有存活对象向内存一端移动,然后清除内存端边界以外的所有空间
graph LR
    subgraph 标记前
    A1[对象A] --- B1[对象B]
    C1[对象C]
    D1[对象D]
    end

    subgraph 标记后
    A2["对象A ✓"] --- B2["对象B ✓"]
    C2[对象C]
    D2["对象D ✓"]
    end

    subgraph 整理后
    A3[对象A] --- B3[对象B] --- D3[对象D] --- 空白["       "]
    end

优点

  • 解决了内存碎片问题
  • 可以分配大对象

缺点

  • 移动对象需要暂停应用线程(Stop-The-World,STW):必须在没有应用线程运行的情况下更新所有引用,确保引用一致性
  • 整理过程比清除更复杂,STW 时间更长

STW 会在安全点(Safe Point)发生,这些是程序执行中的特定位置,如方法调用、循环回跳和异常处理点,此时线程状态一致且可以安全暂停。想象成高速公路的收费站,车辆只能在特定点停下检查。

实际应用: 在老年代垃圾回收中经常使用,因为老年代对象存活率高,复制算法会有较大开销。

3. 复制算法(Copying)

复制算法将内存分为两个相等的区域:From 空间和 To 空间。每次只使用其中一个区域,回收时将存活对象复制到另一个区域,然后清空当前区域。

优点

  • 解决了内存碎片问题
  • 分配效率高(只在一侧分配)
  • 回收效率高

缺点

  • 内存利用率低,相当于只使用了一半的内存
  • 对象存活率高时,复制开销大

这就像搬家时,租两套完全相同的房子,却只住其中一套。当需要大扫除时,把有用的东西搬到空房子,然后把原来的房子彻底清空。虽然浪费空间,但整理起来特别高效。

实际运用: Java 的新生代通常采用复制算法。以 HotSpot 虚拟机为例,新生代被分为 Eden 和两个 Survivor 区域,默认比例为 8:1:1,即 Eden 占 80%,每个 Survivor 占 10%,通过-XX:SurvivorRatio=8配置。

// 示例:新生代对象分配与年龄增长
byte[] buffer = new byte[1024]; // 通常在Eden区分配
// 小对象(<Eden剩余空间)在Eden分配,大对象(如超过-XX:PretenureSizeThreshold,默认3MB)直接进入老年代

// 经过一次Minor GC后,如果buffer仍存活
// 会被复制到一个Survivor区,并增加年龄计数
// 可通过以下参数查看对象年龄分布
// -XX:+PrintTenuringDistribution
/*
Desired survivor size 8388608 bytes, new threshold 15 (max 15)
- age   1:    5172024 bytes,    5172024 total
- age   2:     871376 bytes,    6043400 total
- age   3:     212000 bytes,    6255400 total
*/

当对象在 Survivor 区经过一定次数的 GC 后(默认 15 次,由-XX:MaxTenuringThreshold控制),会被提升到老年代。此外,动态年龄判定也会影响晋升:当 Survivor 空间中相同年龄对象大小总和超过 Survivor 空间的一半,年龄大于或等于该年龄的对象将直接进入老年代。

4. 分代收集算法(Generational Collection)

分代收集算法基于"弱分代假说",即大多数对象生命周期很短,少数对象存活时间长。根据对象的生命周期,将堆分为新生代和老年代:

  • 新生代:存放新创建的对象,大多数对象在这里创建和回收
  • 老年代:存放经过多次 GC 仍然存活的对象

这就像一个学校系统:幼儿园(新生代)孩子来来去去变化快,而大学(老年代)学生相对稳定,对两者采用不同的管理方式更有效率。

graph TD
    subgraph Java堆
    subgraph 新生代
    Eden区 --- S0[Survivor 0]
    Eden区 --- S1[Survivor 1]
    end
    老年代
    end

    Eden区 -.Minor GC后存活.-> S0
    S0 -.下次Minor GC后存活.-> S1["Survivor 1 (年龄+1)"]
    S1 -."年龄≥阈值".-> 老年代

不同代采用不同的回收算法

  • 新生代:复制算法(因为大多数对象都"死"了,复制少量存活对象更高效)
  • 老年代:标记-整理或标记-清除算法(存活对象多,复制成本高)

分代策略奠定了现代 GC 的基础,但随着硬件发展(大内存、多核 CPU),出现了更高效的垃圾回收器实现,如 G1(区域化分代)、ZGC(并发低延迟)等,它们在分代思想基础上进行了进一步优化。

案例: 当我们开发一个缓存系统时,缓存对象通常会长期存活,这些对象会逐渐进入老年代:

public class CacheManager {
    private static final Map<String, Object> CACHE = new ConcurrentHashMap<>();

    public static void put(String key, Object value) {
        CACHE.put(key, value); // 这些对象可能会长期存活,最终进入老年代
    }

    public static Object get(String key) {
        return CACHE.get(key);
    }
}

5. CMS 收集器(Concurrent Mark Sweep)

CMS 是一种以获取最短回收停顿时间为目标的老年代收集器,基于标记-清除算法实现。它的工作流程:

  1. 初始标记:标记 GC Roots 直接关联对象(STW,很短)
  2. 并发标记:并发追踪引用关系
  3. 重新标记:修正并发标记期间变动的部分(STW,较短)
  4. 并发清除:清理死亡对象,与应用线程并发
sequenceDiagram
    participant A as 应用线程
    participant G as CMS线程
    G->>G: 初始标记(STW)
    A->>A: 运行
    G->>G: 并发标记
    A->>A: 运行
    G->>G: 重新标记(STW)
    A->>A: 运行
    G->>G: 并发清除
    A->>A: 运行

优点

  • 并发收集,低延迟(STW 通常在 10-200ms,远低于 Parallel 的 1 秒以上)
  • 适合交互式应用

缺点

  • 内存碎片问题(基于标记-清除,没有整理)
  • 对 CPU 资源敏感,与应用争抢 CPU
  • "并发模式失败"风险:并发清理赶不上应用分配速度时触发 Full GC

适用场景: 对响应速度有要求的中小型应用,JDK9 前使用较多。

6. G1 垃圾回收器(Garbage-First)

G1 是一种面向服务端应用的垃圾回收器,目标是替代 CMS。G1 将堆划分为多个大小相等的区域(Region),不要求物理上连续。

graph TD
    subgraph G1堆内存
    E1[Eden] --- E2[Eden]
    E2 --- E3[Eden]
    S1[Survivor] --- S2[Survivor]
    O1[Old] --- O2[Old]
    O2 --- O3[Old]
    O3 --- O4[Old]
    H[Humongous] --- H2[Humongous]
    end

G1 回收过程:

  1. 初始标记:标记 GC Roots 直接关联的对象(STW)
  2. 并发标记:遍历整个堆的对象图
  3. 最终标记:处理并发标记阶段的变动(STW)
  4. 筛选回收:对各个 Region 回收价值和成本进行排序,优先回收垃圾最多的 Region(价值=Region 内垃圾占比,即 Garbage-First 的由来)。这个阶段需要 STW,会将存活对象复制到空 Region 中。

G1 像城市规划者,把内存分成很多小区域,先重点清理"垃圾率"最高的区域,而不是全城大扫除。这样既能保证效率,又能控制停顿时间。

优点

  • 可预测的停顿时间(可通过-XX:MaxGCPauseMillis设置期望停顿时间,G1 会尽量满足,但非绝对保证)
  • 区域化设计,避免全堆扫描
  • 适合大内存应用

使用场景: 在需要较低 GC 延迟的大内存服务器应用中,G1 是不错的选择,JDK 9 后的默认收集器。常用于 Spring Boot 微服务应用。

// 启用G1垃圾回收器
// java -XX:+UseG1GC -jar myapp.jar

// 设置期望的最大GC停顿时间(目标,非硬性限制)
// java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar myapp.jar

// 设置Region大小(默认根据堆大小自动计算,公式为 2048 * 2^n,范围1MB~32MB)
// java -XX:+UseG1GC -XX:G1HeapRegionSize=4m -jar myapp.jar
// 注意:设置过大会导致大对象分配效率降低,设置过小会增加内存管理开销

G1 的关键内部机制包括:

  • 记忆集(Remembered Set):每个 Region 都有一个 RemSet,记录其他 Region 中的对象引用本 Region 中对象的情况,避免全堆扫描
  • Humongous 区域:专门用于存储大对象(大于等于 Region 大小 50%的对象),若频繁创建大对象,可能导致性能下降

下面是一个简单的例子,说明 Region 大小对大对象处理的影响:

// 假设G1HeapRegionSize=1MB
byte[] largeObj = new byte[600 * 1024]; // 约600KB,正常对象
byte[] hugeObj = new byte[1500 * 1024]; // 约1.5MB,被视为Humongous对象

// 如果设置G1HeapRegionSize=4MB
// 则1.5MB对象不再被视为Humongous对象,可能提高性能

7. ZGC(Z Garbage Collector)

ZGC 是 JDK 11 引入的低延迟垃圾回收器,目标是在任何堆大小下都能将 STW 时间控制在 10ms 以内。

ZGC 的核心特性:

  • 并发处理:标记、整理、复制等阶段几乎都是并发的
  • 基于区域:堆内存分为大小动态变化的区域
  • 内存压缩:并发整理内存,保证内存连续
  • 染色指针(Colored Pointers):利用 64 位系统指针的低 4 位存储对象状态(Marked、Remapped 等),实现并发移动对象

染色指针的工作原理可以类比交通灯:

  • 当对象需要移动时,JVM 设置指针中的特定位(如红灯)
  • 应用线程在访问对象时检查这些标志位
  • 如果发现对象正在移动,会执行特殊逻辑找到新位置
  • 这样对象就能在不停止应用线程的情况下移动
graph LR
    subgraph ZGC工作流程
    A["并发标记:通过染色指针标记存活对象"] --> B["并发整理:移动存活对象到新位置"]
    B --> C["并发重映射:更新所有指向移动后对象的引用"]
    end

优点

  • 极低的延迟(<10ms),不会随堆增大而增加
  • 支持 TB 级别堆
  • 对吞吐量影响小(通常比 Parallel GC 低 5-15%)

适用场景: 对延迟极其敏感的大内存应用,如交易系统、实时分析平台、在线游戏服务器。

// 启用ZGC
// java -XX:+UseZGC -jar myapp.jar

// 设置ZGC区域大小(默认为8MB,当堆超过4TB时自动增大)
// java -XX:+UseZGC -XX:ZGCHeapRegionSize=4m -jar myapp.jar

8. Shenandoah 收集器

Shenandoah 是一款与 ZGC 目标类似的低延迟垃圾回收器,由 Red Hat 开发。它通过并发的标记、复制、整理来减少 STW 时间。

Shenandoah 的独特之处在于它的并发复制算法,使用转发指针(Brooks Pointers)来实现对象移动与应用并发执行。每个对象头中都有一个额外的引用字段,指向对象移动后的新位置。当对象被移动时,旧对象的转发指针会指向新位置,允许应用线程在对象移动过程中继续访问,无需 STW。

想象一下搬家的场景:

  • 普通搬家:所有人必须停止活动,等家具搬完才能继续(STW)
  • Shenandoah 方式:在每件家具上贴便利贴指向新位置,大家可以边搬家边使用
graph TD
    subgraph Shenandoah工作流程
    A[初始标记] --> B[并发标记]
    B --> C[最终标记]
    C --> D[并发清理]
    D --> E["并发复制:通过Brooks指针实现并发移动"]
    E --> F[最终引用更新]
    F --> G[并发清理]
    end

优点

  • 低延迟,STW 通常<10ms
  • 适用于不同大小的堆
  • 对吞吐量影响较小(约 85-95%的 Parallel GC 吞吐量,视工作负载而定)

适用场景: 同样适用于对延迟敏感的应用,如响应式微服务、交互式应用。

// 启用Shenandoah
// java -XX:+UseShenandoahGC -jar myapp.jar

// 启用被动GC模式(Shenandoah 1.0+特性,降低CPU使用,只在内存压力大时GC)
// java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=passive -jar myapp.jar

G1、ZGC 和 Shenandoah 的关键实现对比

这三个现代回收器采用了不同的并发策略和内存管理方案,它们都着力解决跨区域/跨代引用问题,但实现方式不同:

特性G1ZGCShenandoah
并发移动对象否(Evacuation 阶段 STW 复制对象)
指针技术普通指针+RemSet染色指针Brooks 指针
内存开销中等较低较高
区域管理固定大小 Region动态区域固定区域
适用场景Spring 微服务金融交易系统流媒体服务

9. Serial 与 Parallel 回收器

这两个是相对传统的垃圾回收器,分别面向单线程和多线程环境:

  1. Serial 回收器:单线程执行 GC,简单高效,适合客户端应用和小内存场景
  • 在小内存场景下(<4GB),单线程回收器的上下文切换开销更低,适合嵌入式或客户端应用
  1. Parallel 回收器:多线程并行执行 GC,注重吞吐量(应用运行时间/(应用运行时间+GC 时间)),适合后台批处理应用

Serial 就像一个清洁工打扫整栋楼,Parallel 则像一个清洁团队并行工作,效率更高但需要更多资源协调。

// 启用Serial GC
// java -XX:+UseSerialGC -jar myapp.jar

// 启用Parallel GC
// java -XX:+UseParallelGC -jar myapp.jar
// 设置并行GC线程数
// java -XX:+UseParallelGC -XX:ParallelGCThreads=4 -jar myapp.jar

常见 GC 问题诊断流程

对于 Java 应用中的 GC 问题,我们可以按照以下流程进行诊断:

graph TD
    A[应用卡顿/内存溢出] --> B[开启GC日志]
    B --> C{是否频繁Full GC?}
    C -->|是| D{内存泄漏?}
    C -->|否| E{Minor GC时间长?}
    D -->|是| F[使用堆转储分析工具查找泄漏]
    D -->|否| G[检查老年代空间]
    G --> H[调整堆大小或回收器]
    E -->|是| I[检查对象过早晋升]
    I --> J[调整新生代大小]

全量 GC 触发条件与排查

Full GC(或 Major GC)会对整个堆进行回收,包括新生代和老年代,通常会导致较长的 STW 时间。以下是常见的触发条件:

  1. 老年代空间不足:这是最常见的原因
  2. 永久代/元空间不足:JDK 8 前的 PermGen 或之后的 Metaspace 空间不足(可通过-XX:MaxMetaspaceSize限制大小,尤其对使用大量动态类加载的 Spring Boot 等应用重要)
  3. 显式调用System.gc()或 JMX 触发(可通过-XX:+DisableExplicitGC禁用)
  4. CMS 的并发模式失败:CMS 回收时内存分配速度过快
  5. 分配担保失败:Minor GC 前,老年代剩余空间不足以容纳新生代所有存活对象(动态年龄判定后的可能晋升集合),触发 Full GC

分配担保失败类似于银行资金不足的场景:

  • 银行(老年代)必须确保有足够资金(空间)
  • 如果客户(新生代对象)可能同时取款(晋升)的总额大于准备金
  • 银行必须先筹集资金(触发 Full GC)再处理业务

如何排查频繁 Full GC 问题

  1. 使用工具监控 GC
# JDK 8及以下
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log

# JDK 9+统一日志格式
-Xlog:gc*=info:file=gc.log:time,uptime,level,tags

# 使用jstat实时监控
jstat -gcutil <pid> 1000

# 生成堆转储
jmap -dump:format=b,file=heap.bin <pid>
  1. 分析内存占用
# 查看对象分布
jmap -histo <pid> | head -20

# 使用MAT/VisualVM分析堆转储文件

3. 常见原因与解决方案

  • 内存泄漏:对象无法被回收(如静态集合持续增长)
  • 内存过小:增加堆大小(-Xmx)
  • 大对象分配:避免频繁创建大对象,考虑对象池
  • 过早晋升:增加新生代大小(-Xmn)或调整晋升阈值

如何选择合适的垃圾回收器?

选择垃圾回收器需要考虑以下因素:

  1. 应用特点:是否对延迟敏感?内存使用模式如何?
  2. 硬件资源:CPU 核心数、可用内存
  3. 性能需求:吞吐量 vs 延迟
  • 吞吐量:应用程序运行时间占总时间的比例
  • 延迟:单次 GC 造成的停顿时间

各回收器性能对比(大致参考值):

  • Parallel GC:吞吐量 95%+,STW 100-1000ms
  • CMS:吞吐量 85-90%,STW 10-200ms
  • G1:吞吐量 80-90%,STW 50-200ms(可预测)
  • ZGC/Shenandoah:吞吐量 85-95%(视负载而定),STW <10ms

选择建议:

  • 对于需要高吞吐量的批处理系统,可以选择 Parallel GC(允许较长 STW 换取更少 GC 次数)
  • 对于需要低延迟的交互式应用,可以选择 G1、ZGC 或 Shenandoah(拆分 GC 任务,减少单次 STW)
  • 对于内存受限的环境,可能 Serial GC 是更好的选择(小内存场景下单线程回收器的上下文切换开销更低)
graph TD
    开始 --> A{内存大小?}
    A -->|小于4GB| B{对延迟要求?}
    A -->|大于4GB| C{对延迟要求?}
    B -->|不敏感| D[Serial GC]
    B -->|敏感| E[CMS]
    C -->|高吞吐量优先| F[Parallel GC]
    C -->|低延迟优先| G{JDK版本?}
    G -->|JDK8/9| H[G1]
    G -->|JDK11+| I[ZGC/Shenandoah]

JVM 参数最佳实践

不同回收器的推荐参数组合:

G1 回收器参数

# 基本设置
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=8m

# 内存调优
-XX:G1ReservePercent=10     # 预留空间,防止晋升失败
-XX:InitiatingHeapOccupancyPercent=45  # 触发并发标记的堆占用阈值

ZGC 参数

# 基本设置
-XX:+UseZGC
-XX:ZGCHeapRegionSize=8m    # 区域大小

# 内存调优
-XX:+UnlockExperimentalVMOptions
-XX:ZCollectionInterval=300 # 主动GC间隔

Shenandoah 参数

# 基本设置
-XX:+UseShenandoahGC

# 工作模式
-XX:ShenandoahGCMode=passive # 被动模式(另有normal/aggressive)

前沿技术与发展趋势

随着 JDK 的演进,GC 技术也在持续发展:

  1. JDK 17+的结构化并发:通过虚拟线程和结构化并发 API,减少线程栈内存占用,间接影响 GC 行为

  2. GraalVM 与垃圾回收:GraalVM 的 Substrate VM 通过提前编译(AOT)减少运行时动态对象分配,降低 GC 压力

  3. 项目 Leyden:OpenJDK 未来计划,旨在提供更好的静态化能力,可能革新 Java 内存模型和 GC 机制

GC 调优关键原则

  1. 优先优化应用程序,而非盲目调整 JVM 参数
  • 减少临时对象创建
  • 使用对象池管理大对象
  • 避免频繁的装箱/拆箱操作
  1. 明确性能目标
  • 是优化响应时间还是吞吐量?
  • 能接受的最大停顿时间是多少?
  1. 监控实际表现
  • 定期查看 GC 日志
  • 持续监控内存使用趋势

实际案例:一个 Web 应用服务层频繁创建 JSON 对象,修改代码重用对象比调整 GC 参数效果更好:

// 优化前:每次请求创建新对象
public Response process() {
    ObjectMapper mapper = new ObjectMapper(); // 每次都创建新实例
    return mapper.readValue(data, Response.class);
}

// 优化后:使用共享实例
private static final ObjectMapper MAPPER = new ObjectMapper(); // 单例重用
public Response process() {
    return MAPPER.readValue(data, Response.class);
}

GC 日志分析

在生产环境中,通过 GC 日志可以分析垃圾回收行为,帮助调优:

# JDK 8及以下
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log

# JDK 9+统一日志格式
-Xlog:gc*=info:file=gc.log:time,uptime,level,tags

GC 日志示例解读:

[2022-03-15T10:15:30.123+0800] [GC (Allocation Failure) [PSYoungGen: 65536K->8192K(76800K)] 65536K->8200K(251904K), 0.0234700 secs]

这条日志表明:

  • 发生在 2022-03-15 10:15:30
  • 因为"分配失败"触发的 Young GC
  • PSYoungGen 表示使用 Parallel Scavenge 收集器
  • 新生代内存从 65536K 减少到 8192K
  • 总堆内存从 65536K 减少到 8200K
  • GC 耗时 23.47ms

G1 GC 日志示例:

[2022-03-15T10:20:45.678+0800] [GC pause (G1 Evacuation Pause) (young), 0.0345700 secs]
   [Eden: 112.0M(112.0M)->0.0B(96.0M) Survivors: 16.0M->32.0M Heap: 246.8M(512.0M)->198.8M(512.0M)]

通过分析 GC 频率、停顿时间和内存使用模式,可以判断是否需要调整堆大小、回收器类型或参数。关键指标包括:

  • GC 频率(太频繁表示内存压力大)
  • 停顿时间(超过预期可能需要更换回收器)
  • 内存回收效率(回收比例低表示对象大多长期存活)
  • 晋升情况(过早晋升可能导致 Full GC)

总结

下表对 Java 垃圾回收算法和回收器进行了系统分类:

类型算法/回收器核心原理优点缺点适用场景
基础算法标记-清除标记存活对象,清除未标记对象实现简单碎片多、效率低早期 JVM、概念验证
复制存活对象复制到空白区域,清空原区域无碎片、分配高效内存利用率低、高存活对象低效新生代
标记-整理标记后压缩对象到内存一端无碎片、支持大对象移动对象开销、STW老年代
分代策略分代收集按生命周期分代,新生代复制+老年代整理针对性优化实现复杂所有现代 JVM
现代回收器Serial单线程 GC简单高效STW 长客户端、小内存场景
Parallel多线程并行 GC高吞吐量STW 仍较长批处理系统、Kafka 后台
CMS并发标记清除低延迟内存碎片、CPU 敏感交互式应用
G1区域化+筛选回收可预测停顿内存占用略高Spring Boot 微服务
ZGC染色指针+并发移动极低延迟(<10ms)吞吐量略低、JDK11+金融交易、游戏服务器
ShenandoahBrooks 指针+并发复制低延迟(<10ms)内存开销较大流媒体、实时分析