Java 垃圾收集机制:时机、对象与操作全解析
Java 垃圾收集(Garbage Collection,GC)是 JVM 自动管理内存的核心机制,旨在回收不再使用的对象所占用的内存,避免内存泄漏和溢出。以下从触发时机、回收对象、执行操作三个核心维度详细解析,并结合示例代码展示关键场景:
一、垃圾收集的触发时机
GC 并非实时执行,而是在特定条件下触发,分为 Minor GC(新生代 GC)、Major GC(老年代 GC)和 Full GC(整堆 GC)三类:
1. Minor GC(新生代 GC)
- 触发时机:当新生代的 Eden 区满,或新对象分配内存时 Eden 区空间不足。
- 触发频率:频繁(新生代对象生命周期短,回收频繁)。
- 触发原因:新生代采用 "复制算法",Eden 区和 Survivor 区空间有限,对象快速填满后需回收。
2. Major GC(老年代 GC)
- 触发时机:老年代空间不足时触发(如新生代对象晋升到老年代,老年代剩余空间不足)。
- 触发频率:较低(老年代对象生命周期长,回收成本高)。
- 关联操作:Major GC 通常伴随一次 Minor GC("晋升担保" 机制:若 Minor GC 后仍有大量对象需晋升,会触发 Major GC)。
3. Full GC(整堆 GC)
- 触发时机 :
- 老年代空间不足且 Major GC 后仍无法释放足够内存;
- 方法区(元空间)空间不足;
- 调用
System.gc()(仅建议 JVM 执行 GC,不保证立即触发); - CMS GC 出现 "Concurrent Mode Failure"(并发回收时老年代空间不足)。
- 特点:回收整个堆(新生代 + 老年代 + 元空间),暂停时间长,性能影响大。
二、垃圾收集的目标对象:"无用对象"
GC 回收的是堆内存中不再被引用的对象,JVM 通过以下机制判断对象是否 "无用":
1. 引用计数法(早期算法,存在缺陷)
- 原理:为每个对象维护一个引用计数器,被引用时 + 1,引用失效时 - 1;计数器为 0 则视为无用对象。
- 缺陷:无法解决循环引用(如 A 引用 B,B 引用 A,计数器均不为 0,但实际无外部引用),因此现代 JVM 已不再使用。
2. 可达性分析算法(主流算法)
- 原理:以 "GC Roots" 为起点,向下遍历对象引用链,若对象不可达(不在任何引用链上),则视为无用对象。
- GC Roots 包含 :
- 虚拟机栈(栈帧中的局部变量表)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中 JNI(Native 方法)引用的对象;
- JVM 内部的引用(如基本数据类型对应的 Class 对象、异常对象等)。
3. 引用类型与回收策略
Java 中的引用分为 4 类,不同引用类型的对象回收时机不同:
- 强引用 (
Object obj = new Object()):GC 绝不会回收,即使 OOM 也不回收; - 软引用 (
SoftReference):内存不足时回收,用于缓存; - 弱引用 (
WeakReference):GC 时立即回收,用于临时对象; - 虚引用 (
PhantomReference):仅用于跟踪对象回收,必须配合引用队列使用。
三、垃圾收集的核心操作:回收与内存整理
GC 的核心操作包括标记无用对象、回收内存和整理内存碎片,不同垃圾收集器采用不同算法实现:
1. 标记阶段:识别无用对象
通过可达性分析算法标记所有可达对象,未被标记的即为无用对象(待回收)。
2. 回收阶段:释放无用对象内存
根据不同算法,回收方式分为:
-
复制算法(新生代):
- 将新生代分为 Eden 区和两个 Survivor 区(From、To);
- 回收时将 Eden 和 From 区的存活对象复制到 To 区,清空 Eden 和 From 区;
- 交换 From 和 To 区角色,重复此过程;
- 优点:无内存碎片;缺点:浪费部分内存(To 区闲置)。
-
标记 - 清除算法(老年代):
- 标记无用对象后直接清除;
- 优点:无需复制对象;缺点:产生内存碎片,影响后续内存分配。
-
标记 - 整理算法(老年代):
- 标记无用对象后,将存活对象向内存一端移动,再清除边界外的内存;
- 优点:无内存碎片;缺点:需移动对象,成本较高。
3. 内存整理:优化内存布局
- 复制算法天然无碎片;标记 - 整理算法通过移动对象消除碎片;
- 部分收集器(如 CMS)采用 "标记 - 清除",会产生碎片,需通过 Full GC 进行整理。
4. 元空间(方法区)回收
- 回收内容 :无用的类(满足:该类所有实例已回收、类加载器已回收、
Class对象无引用)、常量池中的无用常量; - 触发时机:元空间内存不足时。
四、垃圾收集器的执行特点
- 自动性 :JVM 自动触发,无需程序员手动回收(不同于 C++ 的
delete); - STW(Stop-The-World):多数 GC 操作会暂停所有用户线程(Minor GC 暂停时间短,Full GC 暂停时间长);
- 分代收集:基于对象生命周期将堆分为新生代和老年代,采用不同收集策略(新生代用复制算法,老年代用标记 - 清除 / 整理算法);
- 并发收集:部分收集器(如 CMS、G1)支持并发回收,减少 STW 时间。
五、示例代码
1. 基础示例:对象成为垃圾的场景
java
public class GcBasicExample {
public static void main(String[] args) {
// 创建对象并赋值给强引用
Object obj1 = new Object();
Object obj2 = obj1; // obj2也引用该对象
System.out.println("对象引用状态:obj1=" + obj1 + ", obj2=" + obj2);
// 断开所有引用,对象变为垃圾
obj1 = null;
obj2 = null;
// 建议JVM执行GC(仅建议,不保证立即执行)
System.gc();
System.out.println("已触发GC建议,原对象已无引用");
}
}
2. 引用类型对 GC 的影响示例
(1)软引用(内存不足时回收)
java
import java.lang.ref.SoftReference;
public class SoftReferenceExample {
public static void main(String[] args) {
// 创建软引用指向对象
SoftReference<Object> softRef = new SoftReference<>(new Object() {
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("软引用对象被GC回收");
}
});
System.out.println("GC前软引用获取对象:" + softRef.get());
// 模拟内存不足(需配置JVM参数:-Xmx20M)
try {
byte[] bigArray = new byte[15 * 1024 * 1024]; // 占用15MB内存
} catch (OutOfMemoryError e) {
e.printStackTrace();
}
System.out.println("内存不足后软引用获取对象:" + softRef.get());
}
}
(2)弱引用(GC 时立即回收)
java
import java.lang.ref.WeakReference;
public class WeakReferenceExample {
public static void main(String[] args) {
WeakReference<Object> weakRef = new WeakReference<>(new Object() {
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("弱引用对象被GC回收");
}
});
System.out.println("GC前弱引用获取对象:" + weakRef.get());
// 触发GC
System.gc();
// 暂停线程,确保GC完成
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("GC后弱引用获取对象:" + weakRef.get());
}
}
3. Finalize 方法:对象回收前的操作
java
public class FinalizeExample {
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("FinalizeExample对象即将被GC回收");
}
public static void main(String[] args) {
FinalizeExample obj = new FinalizeExample();
System.out.println("创建对象:" + obj);
// 断开引用
obj = null;
// 触发GC
System.gc();
// 等待GC执行
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
4. 观察 Minor GC 触发(打印 GC 日志)
java
import java.util.ArrayList;
import java.util.List;
public class GcLogExample {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
// 循环创建对象,填满新生代Eden区触发Minor GC
for (int i = 0; i < 100; i++) {
list.add(new byte[1024 * 1024]); // 每次创建1MB对象
System.out.println("已创建第" + (i+1) + "个对象");
}
}
}
运行配置 :需添加 JVM 参数打印 GC 日志-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:+PrintGCTimeStamps(参数说明:堆内存 20MB,新生代 10MB,打印详细 GC 日志)
六、总结
| 维度 | 具体内容 |
|---|---|
| 什么时候 | 新生代 Eden 区满触发 Minor GC;老年代不足触发 Major GC;整堆不足触发 Full GC |
| 对什么 | 堆中不可达的对象(通过可达性分析判断)、方法区的无用类和常量 |
| 做了什么 | 标记无用对象→通过复制 / 标记 - 清除 / 标记 - 整理算法回收内存→整理内存碎片(可选) |
垃圾收集机制的核心目标是自动释放无用内存,平衡回收效率与应用性能,不同垃圾收集器(如 Serial、Parallel、CMS、G1、ZGC)通过优化算法减少 STW 时间,适应不同应用场景的需求。示例代码直观展示了对象成为垃圾的条件、不同引用类型的回收特性及 GC 触发的实际表现。