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 小结

这一节的内容有点多,但都是硬核知识。我帮你梳理一下核心要点:

  1. GC Roots是起点:所有活对象都必须能追溯到GC Roots。泄漏就是“不该活的还活着”。
  2. 强引用是泄漏的根源:90%以上的内存泄漏,都是强引用链没断导致的。
  3. 弱引用是防泄漏的利器:Handler、内部类、观察者模式,优先考虑弱引用。
  4. 软引用和虚引用各有用途:软引用适合缓存(但Android上慎用),虚引用适合资源跟踪。

下一节,我会带你用MAT(Memory Analyzer Tool)实战分析堆转储文件,手把手教你找到那条“罪恶”的引用链。到时候你会发现,理论懂了,工具用起来才顺手。