返回博客列表
技术分享

JVM垃圾回收算法详解

2026年01月31日
4 分钟阅读

JVM垃圾回收算法详解

作为Java开发者,理解JVM的垃圾回收机制是提升应用性能的关键。本文将深入探讨JVM中的四大垃圾回收算法,帮助你选择最适合你应用的GC策略。

什么是垃圾回收?

垃圾回收(Garbage Collection,GC)是Java虚拟机自动管理内存的核心机制。它的主要任务包括:

  • 识别垃圾对象:找出哪些对象不再被使用
  • 回收内存:释放垃圾对象占用的内存空间
  • 内存整理:减少内存碎片,提高内存利用率

判断对象是否存活

在讨论GC算法之前,先了解JVM如何判断对象是否存活:

1. 引用计数法(Reference Counting)

每个对象维护一个引用计数器,当有地方引用它时计数器+1,引用失效时-1。当计数器为0时,该对象就是垃圾。

优点: 实现简单,效率高 缺点: 无法解决循环引用问题

2. 可达性分析(Reachability Analysis)

JVM实际采用的方法。从一系列称为"GC Roots"的对象开始,向下搜索,走过的路径称为引用链。如果一个对象没有任何引用链连接到GC Roots,则该对象不可达,视为垃圾。

GC Roots包括:

  • 虚拟机栈中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象

四大垃圾回收算法

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

执行过程:

  1. 标记:标记所有存活对象
  2. 清除:回收未标记的对象
// 伪代码示例
void markAndSweep() {
    // 1. 标记阶段
    markReachableObjects(gcRoots);

    // 2. 清除阶段
    sweepUnmarkedObjects();
}

优点:

  • 实现简单
  • 不需要移动对象

缺点:

  • 会产生大量内存碎片
  • 效率不高(两次遍历)

标记-清除算法

适用场景: 内存碎片对性能影响不大的场景

2. 复制算法(Copying)

执行过程:

  1. 将内存分为两个相等的区域
  2. 将存活对象复制到另一个区域
  3. 清空当前区域
// 伪代码示例
void copyingGC() {
    // 1. 复制存活对象
    copyReachableObjects(fromSpace, toSpace);

    // 2. 交换from和to空间
    swapSpaces();

    // 3. 清空from空间(已经是新的to空间了)
    clearFromSpace();
}

优点:

  • 不会产生内存碎片
  • 效率高(只需遍历存活对象)
  • 简单高效,适合对象存活率低的情况

缺点:

  • 内存利用率低(只能用一半)

改进: Eden:Survivor:Survivor = 8:1:1

适用场景: 新生代(对象存活率低)

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

执行过程:

  1. 标记所有存活对象
  2. 将存活对象向一端移动
  3. 清理掉边界外的内存
// 伪代码示例
void markAndCompact() {
    // 1. 标记阶段
    markReachableObjects(gcRoots);

    // 2. 整理阶段(移动存活对象)
    compactReachableObjects();

    // 3. 清理阶段
    clearCompactedSpace();
}

优点:

  • 不会产生内存碎片
  • 内存利用率高

缺点:

  • 移动对象需要额外开销

适用场景: 老年代(对象存活率高)

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

根据对象存活周期的不同,将内存划分为几块:

┌─────────────────────────────────────┐
│          Young Generation           │
│  ┌──────┐  ┌────┐  ┌────┐          │
│  │ Eden │  │ S0 │  │ S1 │          │
│  └──────┘  └────┘  └────┘          │
│            8:1:1                   │
├─────────────────────────────────────┤
│        Old Generation              │
├─────────────────────────────────────┤
│         Metaspace                  │
└─────────────────────────────────────┘

分代策略:

  • 新生代:使用复制算法(对象存活率低)
  • 老年代:使用标记-整理或标记-清除算法(对象存活率高)

对象晋升规则:

  1. 新对象分配在Eden区
  2. Eden区满后,触发Minor GC,存活对象移到Survivor区
  3. Survivor区满后,继续复制到另一个Survivor区
  4. 对象年龄达到阈值(默认15),晋升到老年代
  5. 老年代空间不足时,触发Full GC

常见的垃圾收集器

Serial GC

  • 单线程GC
  • 适合客户端应用
  • STW时间较长

Parallel GC

  • 多线程GC
  • 吞吐量优先
  • 适合后台计算任务
-XX:+UseParallelGC

CMS GC(Concurrent Mark Sweep)

  • 低延迟
  • 并发标记和清除
  • 已被G1 GC取代

G1 GC(Garbage First)

  • 面向服务端应用
  • 可预测停顿时间
  • 将堆划分为多个Region
-XX:+UseG1GC

ZGC / Shenandoah

  • 超低延迟
  • 几乎无停顿
  • 适合对延迟要求极高的应用

性能调优建议

1. 合理设置堆内存大小

-Xms2g -Xmx2g

2. 调整新生代和老年代比例

-XX:NewRatio=2  # 新生代:老年代 = 1:2

3. 设置GC日志

-Xlog:gc*:file=gc.log:time,uptime:level,tags

4. 选择合适的GC收集器

根据应用特点选择:

  • 吞吐量优先:Parallel GC
  • 低延迟优先:G1 GC / ZGC
  • 内存受限:CMS GC(已废弃)

总结

算法 优点 缺点 适用场景
标记-清除 简单 内存碎片 内存要求不严格
复制 无碎片、高效 内存利用率低 新生代
标记-整理 无碎片、内存利用率高 移动开销大 老年代
分代收集 综合优化 配置复杂 通用场景

最佳实践:

  • 优先使用分代收集
  • 根据应用特点选择GC收集器
  • 定期监控GC日志,优化参数
  • 避免手动调用System.gc()

理解垃圾回收算法不仅有助于面试,更重要的是在实际开发中能够做出更合理的内存管理决策,提升应用性能。


希望这篇文章对你有帮助!如果你有任何问题或想要深入讨论某个点,欢迎在评论区交流。