JVM 整理(五) 垃圾回收(GC)

Java相比C/C++的一大优势就是自动内存管理,也就是垃圾回收(Garbage Collection,简称GC)。GC机制让开发者无需手动释放对象内存,大大减少了内存泄漏和悬垂指针的风险。然而,自动回收并不意味着我们可以高枕无忧------理解GC的工作原理,是优化Java应用性能、排查内存问题的必备技能。

本文将系统介绍JVM垃圾回收的方方面面,包括内存管理、对象生死判定、垃圾回收算法、分代回收机制、四种引用类型以及主流垃圾收集器,最后还会简要介绍G1中的三色标记算法。

1. 内存管理概述

1.1 从C/C++的手动管理说起

在C/C++中,程序员必须手动分配和释放内存:

cpp 复制代码
Test* test = new Test();
delete test;  // 必须手动释放

如果忘记释放不再使用的对象,就会造成内存泄漏;如果多次释放,则可能导致程序崩溃。这种手动管理方式虽然灵活,但极易出错。

1.2 Java的自动垃圾回收

Java引入了自动垃圾回收机制,由JVM负责回收不再使用的对象。GC主要针对(Heap)进行回收,因为堆中存放了绝大部分对象实例。方法区(元空间)也会回收,但条件苛刻且很少发生。而线程私有的程序计数器、虚拟机栈、本地方法栈随着线程的消亡自然释放,无需GC介入。

2. 方法区的垃圾回收

方法区(JDK 8后为元空间)主要存储类元数据、常量、静态变量等。回收条件比堆严格得多,需要同时满足以下三个条件:

  1. 该类所有的实例都已被回收(堆中不存在任何该类的实例)。

  2. 加载该类的ClassLoader已经被回收。

  3. 该类对应的java.lang.Class对象没有任何地方被引用。

由于条件苛刻,方法区的回收通常发生在Full GC时,且回收效率较低。

3. 判定对象已死:引用计数法与可达性分析

GC要回收对象,首先需要判断哪些对象已经"死亡"(不再被使用)。

3.1 引用计数法

  • 原理:每个对象维护一个引用计数器,每当有一个地方引用它时,计数器+1;引用失效时,计数器-1。计数器为0的对象即可回收。

  • 优点:实现简单,判定高效。

  • 缺点:难以处理循环引用(如A引用B,B引用A,外部再无引用时两者计数器均不为0,无法回收)。主流Java虚拟机已摒弃此法。

3.2 可达性分析算法

  • 原理 :以一系列称为GC Roots 的对象为起点,从这些根向下搜索,走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连(即不可达),则证明此对象已死亡。

  • GC Roots包括

    • 虚拟机栈(栈帧中的局部变量表)中引用的对象。

    • 方法区中静态属性引用的对象。

    • 方法区中常量引用的对象。

    • 本地方法栈中JNI引用的对象。

    • 活跃线程(Thread对象)等。

可达性分析解决了循环引用问题,是Java、C#等现代语言的选择。

4. 垃圾回收算法

确定了哪些对象可回收后,GC需要执行清理。以下是三种基础算法。

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

  • 步骤

    1. 标记:从GC Roots出发,标记所有可达对象。

    2. 清除:遍历堆,将未标记的对象回收。

  • 优点:简单,不需要移动对象。

  • 缺点

    • 效率问题:标记和清除都需要遍历所有对象,效率较低。

    • 空间问题:产生大量内存碎片,导致后续分配大对象困难。

4.2 复制算法(Copying)

  • 原理:将内存分为两块(如From和To),每次只使用一块。当这块内存用完时,将存活对象复制到另一块,然后一次性清理原块。

  • 优点:实现简单,不会产生碎片。

  • 缺点:内存利用率只有一半;如果对象存活率高,复制开销大。

  • 应用:新生代采用复制算法,因为对象"朝生夕死",存活率低。

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

  • 步骤

    1. 标记所有存活对象。

    2. 将存活对象向一端移动,然后清理边界以外的内存。

  • 优点:避免碎片,内存利用率高。

  • 缺点:移动对象需要更新引用,开销较大。

  • 应用:老年代常用此算法(或与标记-清除混合使用)。

4.4 分代收集算法(Generational Collection)

当前商业虚拟机的垃圾收集都采用分代收集,即根据对象存活周期将堆划分为新生代和老年代,分别采用不同算法。

  • 新生代 :对象存活率低,使用复制算法,只需复制少量存活对象,效率高。

  • 老年代 :对象存活率高,使用标记-清除标记-整理算法,避免复制开销。

5. 四种引用类型

Java的引用强度依次递减,不同引用类型决定了对象在GC时的命运。

5.1 强引用(Strong Reference)

  • 特点 :最普通的引用,如Object obj = new Object()。只要强引用存在,GC永远不会回收被引用的对象。

  • 示例User user = new User();

5.2 软引用(Soft Reference)

  • 特点 :内存不足时才会回收。适合实现缓存,如SoftReference<User>

  • 应用:本地缓存、图片缓存等。

5.3 弱引用(Weak Reference)

  • 特点:只要发生GC,无论内存是否充足,都会被回收。适合实现规范化映射(如WeakHashMap)。

  • 应用:ThreadLocal中的Entry就是弱引用。

5.4 虚引用(Phantom Reference)

  • 特点:最弱的引用,无法通过它获取对象实例。唯一目的是在对象被回收时收到一个系统通知(通过引用队列)。

  • 应用:对象回收跟踪、堆外内存释放。

6. 垃圾收集器(GC Collectors)

垃圾收集器是垃圾回收算法的具体实现。不同收集器适用于不同场景,以下是HotSpot虚拟机中的主流收集器。

6.1 Serial / Serial Old

  • 特点:单线程收集,进行垃圾回收时必须暂停所有用户线程(Stop The World,STW)。简单高效,适合客户端模式或单核环境。

  • 新生代:Serial(复制算法)

  • 老年代:Serial Old(标记-整理)

6.2 ParNew

  • 特点:Serial的多线程版本,与CMS配合使用。

  • 新生代:并行复制;老年代仍串行(或配合CMS)。

6.3 Parallel Scavenge / Parallel Old

  • 特点 :关注吞吐量(CPU用于用户代码的时间与总时间的比值)。可动态调整参数,适合后台计算型应用。

  • 新生代:Parallel Scavenge(复制算法)

  • 老年代:Parallel Old(标记-整理)

6.4 CMS(Concurrent Mark Sweep)

  • 特点 :以最短回收停顿时间为目标,适用于互联网服务端。基于"标记-清除",过程分为:

    1. 初始标记(STW,标记GC Roots直接关联对象)

    2. 并发标记(与用户线程并发)

    3. 重新标记(STW,修正并发标记期间的变动)

    4. 并发清除

  • 优点:并发、低停顿。

  • 缺点:产生碎片、占用CPU资源、无法处理浮动垃圾。

  • JDK 9后标记为废弃,JDK 14正式移除

6.5 G1(Garbage First)

  • 特点区域化、分代化、可预测停顿。将堆划分为多个大小相等的Region(默认2048个),每个Region可扮演Eden、Survivor、Old或Humongous(大对象)。不再物理分代,而是逻辑分代。

  • 工作步骤

    1. 初始标记(STW)

    2. 并发标记(与用户线程并发)

    3. 最终标记(STW)

    4. 筛选回收(STW,根据Region回收价值和成本排序,优先回收收益高的Region)

  • 优点 :避免全堆扫描,使用Remembered Set管理跨Region引用;可预测停顿时间(通过-XX:MaxGCPauseMillis指定);不会产生碎片(采用复制算法)。

  • 默认:JDK 9+默认收集器。

6.5.1 三色标记算法(G1并发标记的核心)

三色标记是并发标记的基础,用于在不暂停用户线程的情况下标记存活对象。

  • 白色:尚未访问的对象。

  • 灰色:已被访问,但其引用的对象尚未全部访问。

  • 黑色:已被访问且其所有引用都已访问。

问题:并发标记期间用户线程可能修改引用关系,导致两种异常:

  • 浮动垃圾:本应回收的对象被误标为存活,留待下次GC处理。

  • 漏标(错杀):存活对象被误标为垃圾,导致程序错误。

解决方案

  • CMS 使用增量更新:当黑色对象新增指向白色对象的引用时,记录该黑色对象,在重新标记阶段重新扫描。

  • G1 使用原始快照(SATB):当灰色对象要删除指向白色对象的引用时,在写屏障中记录该引用,保证该白色对象被当作存活(即使实际已死,也当作浮动垃圾)。

6.6 ZGC

  • 特点 :JDK 11引入,专注于极低延迟(几毫秒内完成GC),可处理TB级堆。基于染色指针、读屏障等技术,几乎全部并发,STW时间极短。

  • 适用场景:对延迟极度敏感的超大内存应用。

7. GC触发机制与类型

  • Minor GC / Young GC:新生代空间(Eden)不足时触发。频率高,速度快。

  • Major GC / Old GC:老年代空间不足时触发。通常比Minor GC慢10倍以上。

  • Full GC:回收整个堆和方法区。触发条件包括:

    • 老年代空间不足。

    • 方法区空间不足。

    • 调用System.gc()(建议,不一定立即执行)。

    • 大对象分配失败等。

8. 垃圾收集器选择指南

收集器组合 适用场景
Serial + Serial Old 单核、客户端、小内存应用
Parallel Scavenge + Parallel Old 高吞吐量、后台计算型应用
ParNew + CMS 低延迟、响应优先的应用(JDK 8及以前)
G1 平衡吞吐量与延迟,大内存(6GB+),JDK 9+默认
ZGC 极低延迟、超大内存(数百GB以上)

注意:没有最好的收集器,只有最适合的。建议在实际环境中进行压测,根据吞吐量、停顿时间、内存占用等指标选择。

9. 总结

JVM垃圾回收是一个复杂而精妙的系统,它通过可达性分析判定对象生死,采用分代收集和多种算法平衡效率与内存利用率,并提供多种收集器满足不同场景需求。理解GC原理有助于我们写出更高效的代码,合理设置JVM参数,以及在遇到内存溢出、频繁GC时快速定位问题。

在实际开发中,我们应当:

  • 注意对象的生命周期,避免内存泄漏。

  • 合理使用软引用、弱引用实现缓存。

  • 监控GC日志,及时发现异常。

  • 根据应用特点选择并调优垃圾收集器。

相关推荐
闻哥2 小时前
深入理解 MySQL InnoDB Buffer Pool 的 LRU 冷热数据机制
android·java·jvm·spring boot·mysql·adb·面试
He BianGu2 小时前
【笔记】在WPF中QueryContinueDragEvent的详细介绍
笔记·wpf
He BianGu2 小时前
【笔记】在WPF中QueryCursor事件的功能和应用场景详细介绍
笔记·wpf
booksyhay2 小时前
XCP协议学习笔记
网络·笔记·学习
左左右右左右摇晃2 小时前
JVM 整理(三) 方法区+虚拟机栈
jvm·笔记
NGC_66112 小时前
G1收集器
java·开发语言·jvm
森林里的程序猿猿2 小时前
垃圾收集器ParNew&CMS与底层标记三色标记算法
java·jvm·算法
He BianGu2 小时前
【笔记】在WPF中CommandManager的功能和应用场景详细介绍
笔记·wpf