1. 内存泄漏基础:什么是内存泄漏、GC原理与可达性分析、强/软/弱/虚引用区别
1.1 到底什么是内存泄漏?
先说说我的理解。内存泄漏,说白了就是「该回收的对象,没被回收」。
你想想看,Android系统给每个App分配的内存是有限的。正常情况下,一个对象用完了,GC会把它清理掉,腾出空间。但有些对象明明已经没用了,却因为被某些地方「强引用」着,GC没法动它。
我遇到过最典型的场景——Activity泄漏。用户打开一个页面,旋转屏幕,Activity重建了。但旧的Activity被某个静态变量或者匿名内部类持有,一直活在内存里。用户多旋转几次屏幕,内存就蹭蹭往上涨,最后OOM崩溃。
核心定义:内存泄漏 = 无用对象 + 仍然可达 + GC无法回收
内存泄漏的危害是累积的。一次泄漏可能就几十KB,但日积月累,App就会越来越卡,甚至闪退。我见过一个电商App,用户浏览20个商品详情页后必崩,查下来就是每个页面泄漏了约2MB的Bitmap数据。
1.2 GC原理与可达性分析
聊GC之前,先问一个问题:GC怎么知道哪些对象该回收?
早期JVM用引用计数法。每个对象记一个数字,被引用就+1,引用失效就-1。听起来挺合理对吧?但有个致命问题——循环引用。A引用B,B引用A,两个对象都没人用了,但计数都不为0,GC就傻眼了。
所以现在的Java/Android虚拟机,用的都是可达性分析算法。
这个算法的思路很简单:从一组「GC Roots」出发,沿着引用链往下走,能走到的对象就是「活的」,走不到的就是「死的」,可以回收。
哪些东西可以作为GC Roots?我列一下:
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
- 活跃线程(Thread对象)
举个例子。你写了个单例:
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context ctx) {
this.context = ctx;
}
public static AppManager getInstance(Context ctx) {
if (instance == null) {
instance = new AppManager(ctx);
}
return instance;
}
}
这里instance是静态变量,属于GC Roots。它持有了传入的Context。如果你传入的是Activity的Context,那这个Activity就永远被AppManager「拴住」了,GC没法回收它。这就是典型的静态引用导致的泄漏。
我的经验:排查内存泄漏时,我习惯先用Android Studio的Memory Profiler抓一个Heap Dump,然后用MAT或者Android Studio自带的分析器看「GC Root路径」。找到那条路径,你就找到了泄漏的根源。
1.3 强/软/弱/虚引用,到底怎么用?
Java里引用的强度分四种。我按强度从高到低给你捋一遍。
| 引用类型 | 回收时机 | 典型用途 |
|---|---|---|
| 强引用 | 永不回收(除非不可达) | new出来的对象,日常使用 |
| 软引用 | 内存不足时回收 | 缓存实现,如图片缓存 |
| 弱引用 | 下次GC时立即回收 | Handler、内部类避免泄漏 |
| 虚引用 | 任何时候都可能回收 | 对象回收跟踪,用于NIO DirectBuffer |
强引用
就是最常见的 Object obj = new Object()。只要强引用还在,GC绝对不会碰它。这也是内存泄漏的罪魁祸首——你忘了把引用置null,或者被其他对象持有了。
软引用
软引用在内存充足时不会回收,但快要OOM时,GC会把它干掉。我早期做图片缓存时用过SoftReference,后来发现不太靠谱——因为GC回收软引用的时机不确定,容易导致图片突然被清掉,引发卡顿。现在更推荐用LRU Cache。
// 软引用示例
SoftReference<Bitmap> softRef = new SoftReference<>(bitmap);
Bitmap cachedBitmap = softRef.get(); // 可能为null
弱引用
弱引用就厉害了。只要GC跑一轮,不管内存够不够,弱引用指向的对象都会被回收。这个特性特别适合用来解决Handler泄漏问题。
我曾经踩过一个坑:在Activity里写了个匿名Handler:
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 更新UI
}
};
这个Handler持有外部Activity的引用。如果消息队列里有延迟消息,Activity退出了也回收不了。解决方案就是用弱引用:
static class MyHandler extends Handler {
WeakReference<Activity> weakRef;
MyHandler(Activity activity) {
weakRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
Activity activity = weakRef.get();
if (activity != null) {
// 更新UI
}
}
}
注意:用了弱引用不代表万事大吉。你每次用之前都要判空,因为对象随时可能被回收。我见过有人用了WeakReference但不判空,结果拿到null直接崩溃——这比内存泄漏还严重。
虚引用
虚引用是最弱的。你甚至没法通过它拿到对象实例。它的唯一作用就是:当对象被回收时,会收到一个通知。这个在NIO的DirectBuffer里用得比较多,用来跟踪堆外内存的回收。
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue);
// 当obj被回收时,phantomRef会进入queue
1.4 避坑指南
最后分享几个我踩过的坑:
- 静态集合类:我曾经把一个对象放到static的ArrayList里,忘了移除。结果这个对象和它引用的所有东西都泄漏了。现在我的习惯是,能用局部变量就别用静态变量。
- 内部类:非静态内部类默认持有外部类的引用。写回调、Runnable时,要么用静态内部类+弱引用,要么在onDestroy里把引用置null。
- 注册监听器:注册了BroadcastReceiver、EventBus、观察者模式,一定要在合适的时机反注册。我见过有人只注册不反注册,泄漏得悄无声息。
一个小技巧:如果你不确定某个对象会不会泄漏,可以在onDestroy里打一行日志,打印一下引用情况。或者用LeakCanary,它会自动帮你检测Activity和Fragment的泄漏,省心很多。
嗯,内存泄漏的基础就这些。说白了就是一句话:搞清楚对象的生命周期,别让不该活着的对象赖着不走。下一章我会讲具体的排查工具和实战案例,到时候咱们再细聊。