2、GC机制详解:Java垃圾回收机制、GC Roots概念、引用类型对泄漏的影响
聊内存泄漏,绕不开垃圾回收(GC)。
很多人觉得GC是Java的“自动挡”,不用操心。但说实话,正是这种“自动”的错觉,才让内存泄漏变得防不胜防。你想想看,如果GC能搞定一切,那Android应用为什么还会OOM?
这一节,我就带你彻底搞懂GC的底层逻辑。搞懂了它,你才能知道内存到底是怎么“漏”出去的。
2.1 Java垃圾回收机制:到底在回收什么?
GC回收的对象,说白了就是“死掉的对象”。
怎么判断一个对象死了?两种主流算法:引用计数法和可达性分析。
- 引用计数法:每个对象有个计数器,被引用就+1,引用失效就-1。计数器为0时回收。听起来简单,但它解决不了循环引用的问题。比如A引用B,B引用A,俩对象都没人用了,但计数器都不为0。Java没选这条路。
- 可达性分析(Reachability Analysis):这是Java、Android(ART虚拟机)采用的方法。从一组根对象(GC Roots)出发,像剥洋葱一样往下遍历。能遍历到的对象,就是“活的”;遍历不到的,就是“垃圾”。
核心结论:GC只回收“从GC Roots不可达”的对象。如果一个对象明明没用了,但还被GC Roots间接引用着,它就永远不会被回收——这就是内存泄漏的根源。
2.2 GC Roots:内存泄漏的“总开关”
GC Roots是垃圾回收的起点。我习惯把它想象成“根系统”——所有活着的对象,都必须能追溯到这些根。
在Android(ART)中,常见的GC Roots包括:
| GC Roots类型 | 说明 | 泄漏风险 |
|---|---|---|
| 静态变量(static fields) | 类加载时就存在,生命周期与类一致 | 极高。静态集合类持有Activity引用,是泄漏重灾区 |
| 活跃线程(Thread) | 正在运行的线程对象 | 高。线程内部持有外部引用,线程不死,对象不灭 |
| JNI全局引用(GlobalRef) | Native层持有的Java对象引用 | 极高。忘记DeleteGlobalRef,对象永远无法回收 |
| 栈帧中的局部变量 | 方法执行期间,栈帧里的引用 | 低。方法结束即释放,但长耗时操作要注意 |
| 系统类对象(Class对象) | 加载的类本身 | 低。通常不会泄漏,但自定义ClassLoader要注意 |
我在项目中遇到过最典型的案例:一个单例的UserManager,里面用static List<Listener>保存了所有注册的监听器。某个Activity注册了监听器,但销毁时忘了注销。结果呢?Activity被这个静态列表引用着,GC Roots可达,永远无法回收。这就是典型的“静态集合泄漏”。
避坑指南:我曾经排查过一个线上OOM问题,最后发现是某个第三方SDK在JNI层创建了GlobalRef,但SDK版本有bug,没有在适当时候释放。Native层泄漏比Java层更难定位,建议在关键JNI调用处加日志,或者用WeakReference替代GlobalRef。
2.3 引用类型:强、软、弱、虚
Java提供了四种引用类型。它们的区别,说白了就是“GC对引用的态度”不同。
| 引用类型 | GC回收时机 | 典型用途 | 泄漏风险 |
|---|---|---|---|
| 强引用(StrongReference) | 永不回收(除非不可达) | 日常new对象 | 最高。只要引用链存在,GC无权回收 |
| 软引用(SoftReference) | 内存不足时回收 | 缓存、图片池 | 低。但Android 9+后回收策略变激进,慎用 |
| 弱引用(WeakReference) | 下次GC时立即回收 | Handler、内部类、观察者模式 | 极低。防泄漏首选 |
| 虚引用(PhantomReference) | 任何时候都可能回收 | 对象回收跟踪、DirectByteBuffer清理 | 无。无法通过它获取对象 |
2.3.1 强引用:泄漏的“罪魁祸首”
强引用就是最常见的引用:Object obj = new Object()。只要强引用还存在,GC就认为这个对象“活着”。
内存泄漏,绝大多数都是强引用导致的。比如静态变量持有Activity、内部类隐式持有外部类引用、未注销的监听器……这些都是强引用链没断。
2.3.2 软引用:缓存的好帮手,但别太依赖
软引用在内存充足时不会被回收,内存紧张时才会被GC干掉。听起来很适合做缓存,对吧?
但我建议你谨慎使用。Android 9(API 28)之后,系统对软引用的回收变得非常激进。我遇到过的情况是:明明内存还够,软引用却被提前回收了,导致缓存命中率骤降,反而引发频繁的IO或网络请求。
我的建议:在Android上做缓存,优先考虑LruCache。它基于LRU算法,回收策略更可控。软引用可以作为辅助,但别当主力。
2.3.3 弱引用:防泄漏的“银弹”
弱引用是解决内存泄漏的利器。它的特点是:只要GC发生,弱引用指向的对象就会被回收,不管内存够不够。
最常见的应用场景:
- Handler + 内部类:用
static内部类 +WeakReference<Activity>,避免Handler持有Activity导致泄漏。 - 观察者模式:用
WeakReference包装观察者,避免注册后忘记注销。 - Fragment与Activity通信:用弱引用持有Activity引用,避免Fragment在Activity销毁后还持有它。
// 典型用法:Handler防泄漏
private static class MyHandler extends Handler {
private final WeakReference<MainActivity> mActivityRef;
MyHandler(MainActivity activity) {
mActivityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = mActivityRef.get();
if (activity == null) {
return; // Activity已被回收,直接返回
}
// 处理消息
}
}
嗯,这里要注意:弱引用虽然好,但别滥用。如果一个对象确实需要长期存活(比如全局配置),用弱引用反而会导致它被频繁回收,影响性能。
2.3.4 虚引用:最“虚”的引用
虚引用是最弱的引用。你甚至无法通过它获取到对象实例(get()方法永远返回null)。它的唯一作用就是:当对象被回收时,收到一个系统通知。
实际开发中,虚引用用得不多。但在一些底层场景中,它很有用:
- DirectByteBuffer回收:Java的堆外内存(DirectByteBuffer)无法被常规GC回收,需要借助虚引用 + ReferenceQueue来跟踪回收时机。
- 资源清理:比如文件句柄、Socket连接,可以在虚引用触发时做清理。
一句话总结:强引用是“必须活着”,软引用是“尽量活着”,弱引用是“活着也行,死了拉倒”,虚引用是“死了通知我一声”。
2.4 引用类型对泄漏的影响:实战视角
搞清楚了引用类型,我们再回头看内存泄漏。泄漏的本质是什么?
说白了,就是本应被回收的对象,因为被强引用链连接到了GC Roots,导致GC无法回收。
举个例子:
// 泄漏场景:静态集合持有Activity
public class LeakExample {
private static List<Activity> sActivities = new ArrayList<>();
public static void addActivity(Activity activity) {
sActivities.add(activity); // 强引用!Activity被静态列表持有
}
public static void removeActivity(Activity activity) {
sActivities.remove(activity); // 如果忘记调用,Activity就泄漏了
}
}
解决方案很简单:把强引用改成弱引用。
// 修复方案:使用弱引用
public class LeakExample {
private static List<WeakReference<Activity>> sActivities = new ArrayList<>();
public static void addActivity(Activity activity) {
sActivities.add(new WeakReference<>(activity));
}
// 注意:遍历时需要检查弱引用是否已被回收
public static void doSomething() {
Iterator<WeakReference<Activity>> it = sActivities.iterator();
while (it.hasNext()) {
Activity activity = it.next().get();
if (activity == null) {
it.remove(); // 清理已被回收的引用
continue;
}
// 处理activity
}
}
}
重要提醒:弱引用不是万能的。如果一个对象被弱引用持有,但还有其他强引用链指向它,它依然不会被回收。弱引用只解决“唯一引用是弱引用”的情况。排查泄漏时,还是要从GC Roots出发,找到那条“不该存在”的强引用链。
2.5 小结
这一节的内容有点多,但都是硬核知识。我帮你梳理一下核心要点:
- GC Roots是起点:所有活对象都必须能追溯到GC Roots。泄漏就是“不该活的还活着”。
- 强引用是泄漏的根源:90%以上的内存泄漏,都是强引用链没断导致的。
- 弱引用是防泄漏的利器:Handler、内部类、观察者模式,优先考虑弱引用。
- 软引用和虚引用各有用途:软引用适合缓存(但Android上慎用),虚引用适合资源跟踪。
下一节,我会带你用MAT(Memory Analyzer Tool)实战分析堆转储文件,手把手教你找到那条“罪恶”的引用链。到时候你会发现,理论懂了,工具用起来才顺手。