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后为元空间)主要存储类元数据、常量、静态变量等。回收条件比堆严格得多,需要同时满足以下三个条件:
-
该类所有的实例都已被回收(堆中不存在任何该类的实例)。
-
加载该类的
ClassLoader已经被回收。 -
该类对应的
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)
-
步骤:
-
标记:从GC Roots出发,标记所有可达对象。
-
清除:遍历堆,将未标记的对象回收。
-
-
优点:简单,不需要移动对象。
-
缺点:
-
效率问题:标记和清除都需要遍历所有对象,效率较低。
-
空间问题:产生大量内存碎片,导致后续分配大对象困难。
-
4.2 复制算法(Copying)
-
原理:将内存分为两块(如From和To),每次只使用一块。当这块内存用完时,将存活对象复制到另一块,然后一次性清理原块。
-
优点:实现简单,不会产生碎片。
-
缺点:内存利用率只有一半;如果对象存活率高,复制开销大。
-
应用:新生代采用复制算法,因为对象"朝生夕死",存活率低。
4.3 标记-整理算法(Mark-Compact)
-
步骤:
-
标记所有存活对象。
-
将存活对象向一端移动,然后清理边界以外的内存。
-
-
优点:避免碎片,内存利用率高。
-
缺点:移动对象需要更新引用,开销较大。
-
应用:老年代常用此算法(或与标记-清除混合使用)。
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)
-
特点 :以最短回收停顿时间为目标,适用于互联网服务端。基于"标记-清除",过程分为:
-
初始标记(STW,标记GC Roots直接关联对象)
-
并发标记(与用户线程并发)
-
重新标记(STW,修正并发标记期间的变动)
-
并发清除
-
-
优点:并发、低停顿。
-
缺点:产生碎片、占用CPU资源、无法处理浮动垃圾。
-
JDK 9后标记为废弃,JDK 14正式移除。
6.5 G1(Garbage First)
-
特点 :区域化、分代化、可预测停顿。将堆划分为多个大小相等的Region(默认2048个),每个Region可扮演Eden、Survivor、Old或Humongous(大对象)。不再物理分代,而是逻辑分代。
-
工作步骤:
-
初始标记(STW)
-
并发标记(与用户线程并发)
-
最终标记(STW)
-
筛选回收(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日志,及时发现异常。
-
根据应用特点选择并调优垃圾收集器。