CSDN JVM底层原理系列:本文深入探讨JVM中垃圾判定的核心算法与实现机制。从引用计数法到可达性分析,从GC Roots到引用类型,全面解析对象存活判定的技术细节。通过内存布局图解、算法流程详解、实战案例分析,帮助开发者深入理解垃圾回收的工作原理,避免内存泄漏,写出更高效的Java代码。
🎯 一、核心问题:什么是垃圾?
1.1 垃圾对象的定义
在Java中,垃圾(Garbage) 指的是:
程序中已经不再被任何地方使用的对象,这些对象占用的内存可以被安全回收。
1.2 垃圾判定的重要性
| 判定结果 | 后果 | 影响 |
|---|---|---|
| 正确判定为垃圾 | 内存被回收 | 程序正常运行,内存高效利用 |
| 错误判定为垃圾 | 对象被误删 | 程序出现异常,数据丢失(严重bug) |
| 垃圾未被判定 | 内存泄漏 | 内存占用持续增长,最终OOM |
// 示例:垃圾对象的产生
public class GarbageExample {
public void createGarbage() {
// 1. 对象失去引用,变成垃圾
String temp = new String("临时对象");
temp = null; // 前一个String对象变成垃圾
// 2. 局部变量超出作用域
if (true) {
String localVar = new String("局部对象");
} // localVar指向的对象变成垃圾
// 3. 集合元素被移除
List<String> list = new ArrayList<>();
list.add("元素1");
list.remove(0); // "元素1"变成垃圾
}
}
🔍 二、垃圾判定算法详解
2.1 引用计数法(Reference Counting)
算法原理
/**
* 引用计数法的简单实现(概念演示)
*/
public class ReferenceCounting {
private int count = 0; // 引用计数器
private Object data;
public ReferenceCounting(Object data) {
this.data = data;
this.count = 1; // 创建时引用计数为1
}
// 增加引用
public void addReference() {
count++;
System.out.println("引用增加,当前计数: " + count);
}
// 减少引用
public void removeReference() {
count--;
System.out.println("引用减少,当前计数: " + count);
// 当引用计数为0时,对象成为垃圾
if (count == 0) {
System.out.println("对象成为垃圾,可以回收");
cleanup();
}
}
private void cleanup() {
// 释放资源
this.data = null;
}
}
// 使用示例
public class ReferenceCountingDemo {
public static void main(String[] args) {
Object data = new Object();
ReferenceCounting obj = new ReferenceCounting(data);
obj.addReference(); // 计数=2
obj.removeReference(); // 计数=1
obj.removeReference(); // 计数=0 → 垃圾
}
}
优缺点分析
优点:
- ✅ 实现简单
- ✅ 实时性高,对象不再被引用时立即回收
致命缺点:
- ❌ 循环引用问题 - 导致内存泄漏
// 循环引用示例
class Node {
Node next;
String data;
public Node(String data) {
this.data = data;
}
}
public class CyclicReferenceDemo {
public static void main(String[] args) {
Node a = new Node("A");
Node b = new Node("B");
// 创建循环引用
a.next = b; // a引用b
b.next = a; // b引用a
// 即使外部不再引用,引用计数也不为0
a = null;
b = null;
// 但a和b仍然互相引用,无法被回收 → 内存泄漏!
}
}
正是由于循环引用问题,Java没有采用引用计数法。
2.2 可达性分析算法(Reachability Analysis)
算法原理:GC Roots溯源
算法核心思想:
从一组称为 GC Roots 的根对象出发,沿着引用链遍历所有可达对象。所有不可达的对象都被判定为垃圾。
算法实现伪代码
/**
* 可达性分析算法伪代码
*/
public class ReachabilityAnalyzer {
public Set<Object> findGarbage(Set<Object> allObjects, Set<Object> gcRoots) {
Set<Object> reachable = new HashSet<>();
Set<Object> visited = new HashSet<>();
Stack<Object> stack = new Stack<>();
// 1. 从GC Roots开始遍历
for (Object root : gcRoots) {
stack.push(root);
}
// 2. 深度优先遍历引用链
while (!stack.isEmpty()) {
Object current = stack.pop();
if (visited.contains(current)) continue;
visited.add(current);
reachable.add(current);
// 获取当前对象引用的所有对象
for (Object reference : getReferences(current)) {
if (!visited.contains(reference)) {
stack.push(reference);
}
}
}
// 3. 不可达的对象就是垃圾
Set<Object> garbage = new HashSet<>(allObjects);
garbage.removeAll(reachable);
return garbage;
}
private List<Object> getReferences(Object obj) {
// 通过反射获取对象的引用字段值
// 实际JVM中通过对象头、指针映射等机制实现
return new ArrayList<>();
}
}
🌳 三、GC Roots详解:哪些对象是根?
3.1 GC Roots的具体类型
| GC Roots类型 | 具体对象示例 | 生命周期 | 说明 |
|---|---|---|---|
| 虚拟机栈引用的对象 | 局部变量表 | 线程生命周期 | 每个线程栈帧中的局部变量 |
| 方法区静态属性引用的对象 | 静态变量 | 类生命周期 | static修饰的类变量 |
| 方法区常量引用的对象 | 字符串常量 | 类生命周期 | final static常量 |
| 本地方法栈JNI引用的对象 | Native方法参数 | Native调用期间 | JNI引用对象 |
| 虚拟机内部引用对象 | 系统类对象 | JVM生命周期 | Class对象、异常对象等 |
| 同步锁持有的对象 | synchronized锁 | 同步块期间 | 被同步锁持有的对象 |
3.2 GC Roots实战分析
/**
* 演示不同类型的GC Roots
*/
public class GCRootsDemo {
// GC Root 1: 静态变量引用的对象
private static Object staticObj = new Object();
// GC Root 2: 常量引用的对象
private static final String CONSTANT_STR = "常量字符串";
public void demonstrateGCRoots() {
// GC Root 3: 栈帧中的局部变量
Object localObj = new Object();
// GC Root 4: 活跃线程引用的对象
Thread currentThread = Thread.currentThread();
// 方法参数也是GC Root
processObject(localObj);
// 同步锁对象也是GC Root
synchronized (localObj) {
System.out.println("在同步块中");
}
}
private void processObject(Object param) { // param是GC Root
// 方法内的局部变量也是GC Root
Object methodLocal = new Object();
}
// GC Root 5: Class对象和异常对象
public void handleException() {
try {
riskyOperation();
} catch (Exception e) { // 异常对象是GC Root
e.printStackTrace();
}
}
}
3.3 内存快照分析实战
使用MAT(Memory Analyzer Tool)分析GC Roots:
// 制造内存泄漏的示例
public class MemoryLeakExample {
private static List<Object> leakList = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
Object data = new byte[1024 * 1024]; // 1MB
leakList.add(data); // 静态集合持有引用,导致无法回收
}
// 生成堆转储文件
// 命令行: jmap -dump:live,format=b,file=heapdump.hprof <pid>
}
}
MAT分析步骤:
- 使用
jmap生成堆转储文件 - 在MAT中打开hprof文件
- 查看
GC Roots路径 - 分析
leakList的引用链 - 确认内存泄漏根源
🔗 四、引用类型与垃圾判定
4.1 四种引用类型对比
| 引用类型 | 回收时机 | 使用场景 | 对GC的影响 |
|---|---|---|---|
| 强引用(Strong Reference) | 永远不回收(除非不可达) | 普通对象引用 | 必须不可达才回收 |
| 软引用(Soft Reference) | 内存不足时回收 | 缓存实现 | 内存敏感时回收 |
| 弱引用(Weak Reference) | 下次GC时回收 | 弱缓存、监听器 | 无论内存是否充足都回收 |
| 虚引用(Phantom Reference) | 任何时候都可能回收 | 对象回收跟踪 | 不影响对象生命周期 |
4.2 引用类型代码示例
import java.lang.ref.*;
/**
* 不同引用类型对垃圾回收的影响
*/
public class ReferenceTypesDemo {
public static void main(String[] args) throws InterruptedException {
// 1. 强引用 - 最普通的引用
Object strongRef = new Object();
// 2. 软引用 - 内存不足时回收
SoftReference<Object> softRef = new SoftReference<>(new Object());
// 3. 弱引用 - GC时立即回收
WeakReference<Object> weakRef = new WeakReference<>(new Object());
// 4. 虚引用 - 主要用于跟踪对象回收
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
System.out.println("初始状态:");
System.out.println("强引用: " + strongRef);
System.out.println("软引用: " + softRef.get());
System.out.println("弱引用: " + weakRef.get());
System.out.println("虚引用: " + phantomRef.get()); // 总是返回null
// 模拟内存压力,触发GC
System.gc();
Thread.sleep(1000);
System.out.println("\nGC后状态:");
System.out.println("强引用: " + strongRef); // 仍然存在
System.out.println("软引用: " + softRef.get()); // 可能还存在(取决于内存)
System.out.println("弱引用: " + weakRef.get()); // 很可能为null
System.out.println("虚引用队列: " + queue.poll()); // 可能收到通知
// 让强引用不可达,使其成为垃圾
strongRef = null;
System.gc();
System.out.println("\n强引用置null后GC:");
System.out.println("强引用: " + strongRef); // null
}
}
4.3 引用在内存泄漏防护中的应用
/**
* 使用弱引用防止内存泄漏
*/
public class WeakReferenceCache {
private final Map<String, WeakReference<ExpensiveObject>> cache = new HashMap<>();
public ExpensiveObject get(String key) {
WeakReference<ExpensiveObject> ref = cache.get(key);
if (ref != null) {
ExpensiveObject obj = ref.get();
if (obj != null) {
return obj; // 缓存命中
} else {
cache.remove(key); // 清除已回收的条目
}
}
// 缓存未命中,创建新对象
ExpensiveObject newObj = createExpensiveObject(key);
cache.put(key, new WeakReference<>(newObj));
return newObj;
}
private ExpensiveObject createExpensiveObject(String key) {
// 创建昂贵的对象
return new ExpensiveObject(key);
}
}
🛠️ 五、实战:检测和解决内存泄漏
5.1 常见内存泄漏场景
/**
* 典型的内存泄漏示例
*/
public class CommonMemoryLeaks {
// 场景1: 静态集合引起的内存泄漏
private static Map<String, Object> staticCache = new HashMap<>();
public void leak1() {
for (int i = 0; i < 10000; i++) {
String key = "key-" + i;
staticCache.put(key, new byte[1024]); // 对象永远无法回收
}
}
// 场景2: 监听器未正确移除
private List<EventListener> listeners = new ArrayList<>();
public void leak2() {
SomeComponent component = new SomeComponent();
EventListener listener = event -> System.out.println("事件处理");
component.addListener(listener);
// 忘记移除: component.removeListener(listener);
}
// 场景3: 内部类持有外部类引用
public void leak3() {
OuterClass outer = new OuterClass();
outer.createInnerClass();
// innerClass隐式持有outer引用
}
}
class OuterClass {
private byte[] data = new byte[1024 * 1024];
class InnerClass {
// 隐式持有OuterClass.this引用
}
void createInnerClass() {
InnerClass inner = new InnerClass();
}
}
5.2 内存泄漏检测工具链
工具组合使用:
| 工具 | 用途 | 命令示例 |
|---|---|---|
| jps | 查看Java进程 | jps -l |
| jstat | 监控GC状态 | jstat -gc <pid> 1s |
| jmap | 生成堆转储 | jmap -dump:format=b,file=heap.hprof <pid> |
| MAT | 分析内存快照 | 图形化分析工具 |
| VisualVM | 实时监控 | 图形化监控工具 |
5.3 解决方案与最佳实践
/**
* 内存泄漏防护的最佳实践
*/
public class MemoryLeakPrevention {
// 方案1: 使用WeakHashMap
private Map<String, WeakReference<Object>> safeCache = new WeakHashMap<>();
// 方案2: 及时清理资源
public void safeResourceUsage() {
try (Connection conn = getConnection();
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM table")) {
// 自动资源管理
} catch (SQLException e) {
e.printStackTrace();
}
}
// 方案3: 避免内部类内存泄漏
public void safeInnerClassUsage() {
// 使用静态内部类 + 弱引用
Outer outer = new Outer();
Outer.StaticInner inner = outer.new StaticInner();
}
static class Outer {
private byte[] data = new byte[1024 * 1024];
// 静态内部类不持有外部类引用
static class StaticInner {
void doSomething() {
// 需要外部类实例时,通过参数传入
}
}
}
}
💎 总结与核心要点
垃圾判定流程总结
关键知识点回顾
- 判定算法:Java使用可达性分析,而非引用计数
- GC Roots:包括栈引用、静态变量、常量等根对象
- 引用类型:强、软、弱、虚引用影响回收时机
- 内存泄漏:对象可达但实际已不再使用
- 检测工具:jmap、MAT、VisualVM等工具链
实战建议
代码编写阶段:
- ✅ 避免长生命周期的集合持有短生命周期对象
- ✅ 使用弱引用管理缓存
- ✅ 及时关闭资源、移除监听器
- ✅ 谨慎使用内部类
问题排查阶段:
- ✅ 定期监控GC日志和内存使用
- ✅ 使用MAT分析可疑的内存快照
- ✅ 关注
GC Roots引用链分析
💡 核心认知:理解垃圾判定机制不仅是解决内存问题的基础,更是编写高性能、高稳定性Java程序的关键。
💬 互动话题:你在项目中遇到过哪些内存泄漏问题?是如何发现和解决的?欢迎分享你的实战经验!



1万+

被折叠的 条评论
为什么被折叠?



