4、内存优化(上):Java内存模型、GC机制、内存泄漏常见场景
大家好,我是你们的老朋友。今天咱们聊聊内存优化,这个话题我做了这么多年Android,每次面试必问,每次项目必踩坑。说实在的,内存问题就像慢性病,平时不疼不痒,一旦爆发就是ANR、OOM,直接让用户卸载你的App。
这一章我们先啃上半部分:Java内存模型、GC机制,还有那些年我们踩过的内存泄漏坑。嗯,准备好了吗?
4.1 Java内存模型:别再说“堆和栈”就完了
很多人一聊内存模型,张口就是“堆存对象,栈存引用”。这话没错,但太粗糙了。我习惯把Java内存模型拆成三个核心区域来理解:
| 区域 | 存储内容 | 生命周期 | 是否共享 |
|---|---|---|---|
| 堆(Heap) | 对象实例、数组 | GC管理,存活时间不定 | 线程共享 |
| 栈(Stack) | 局部变量、方法调用帧 | 方法结束即销毁 | 线程私有 |
| 方法区(Method Area) | 类信息、常量、静态变量 | 类卸载时释放 | 线程共享 |
这里有个细节我特别想强调:静态变量是存在方法区的,不是堆里。我在项目中遇到过好几次,有人把大Bitmap赋值给静态变量,以为“反正GC会回收”,结果方法区里的引用一直活着,Bitmap根本释放不了。你想想看,这坑踩得多冤?
核心要点:Java内存模型决定了对象的存活路径。一个对象只要从GC Roots出发能到达,就不会被回收。GC Roots包括:栈帧中的局部变量、静态变量、活跃的线程、JNI引用等。
4.2 GC机制:别怕,它没那么玄乎
GC(垃圾回收)说白了就是帮我们打扫内存卫生的。但很多人一听到“Stop The World”就紧张,其实大可不必。Android从5.0开始全面使用ART虚拟机,GC效率比Dalvik时代好太多了。
ART的GC主要分几种:
- Concurrent GC:大部分工作和应用线程并发执行,暂停时间很短
- Full GC:全堆回收,暂停时间较长,一般发生在内存紧张时
- Explicit GC:主动调用System.gc(),我建议你永远别主动调
为什么会这样?因为主动调GC不仅不会立即生效(只是建议),还会触发不必要的Full GC,反而拖慢性能。我曾经在一个项目里看到同事在循环里调System.gc(),结果页面卡成PPT。嗯,后来我帮他改成了对象池复用,问题直接解决。
我的建议:不要试图“帮”GC做决定。你只需要保证对象不再被引用,GC自然会处理。过度干预反而适得其反。
4.3 内存泄漏常见场景:这些坑我全踩过
内存泄漏,说白了就是“该回收的对象回收不了”。下面这几个场景,我敢说每个Android开发都遇到过,只是有些人还没意识到。
4.3.1 Handler泄漏:经典中的经典
这个太常见了。匿名内部类Handler持有外部Activity的引用,如果消息队列里还有未处理的消息,Activity就泄漏了。
// 错误写法
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 更新UI
}
};
// 正确写法:静态内部类 + 弱引用
private static class SafeHandler extends Handler {
private final WeakReference<Activity> activityRef;
SafeHandler(Activity activity) {
activityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
Activity activity = activityRef.get();
if (activity == null) return;
// 更新UI
}
}
我在项目中遇到过最离谱的一次:一个同事在Fragment里写了Handler,然后Fragment被销毁了,但Handler里有个延迟5秒的消息。结果用户反复进出这个页面,内存直接飙到200MB。嗯,后来我们加了个removeCallbacksAndMessages(null)在onDestroy里。
避坑指南:我曾经以为只要用静态Handler就万事大吉,结果忘了在Activity销毁时清空消息队列。记住:静态Handler只是解决了内部类持有外部引用的问题,但消息队列里的回调依然可能引用已销毁的Context。
4.3.2 静态变量泄漏:最隐蔽的杀手
静态变量的生命周期和Application一样长。如果你把一个Activity或View赋值给静态变量,那这个Activity就永远别想被回收了。
public class LeakExample {
private static Context sContext; // 千万别这么干!
public static void setContext(Context context) {
sContext = context; // 如果传入的是Activity,它就泄漏了
}
}
正确的做法是:能用Application Context就别用Activity Context。我习惯在Application类里保存一个全局Context引用,需要时直接拿,既安全又方便。
4.3.3 单例模式泄漏:设计模式的陷阱
单例本身没问题,但单例里持有Activity引用就有问题了。比如一个网络请求的单例,里面保存了回调接口,而回调接口又引用了Activity。
public class NetworkManager {
private static NetworkManager instance;
private Callback callback; // 这里可能泄漏
public static NetworkManager getInstance() {
if (instance == null) {
instance = new NetworkManager();
}
return instance;
}
public void setCallback(Callback callback) {
this.callback = callback; // 如果callback是匿名内部类,就泄漏了
}
}
解决方案很简单:使用弱引用,或者在不需要时主动置空。我个人更倾向于后者,因为弱引用虽然能防止泄漏,但回调可能被GC提前回收,导致逻辑异常。
4.3.4 匿名内部类泄漏:Lambda也逃不掉
很多人觉得用了Lambda就不会有内存泄漏,其实不然。Lambda只是语法糖,底层还是匿名内部类。只要它捕获了外部类的引用,一样会泄漏。
// 看似安全的Lambda
button.setOnClickListener(v -> {
// 如果这里访问了Activity的成员变量,Lambda就持有了Activity的引用
textView.setText("clicked");
});
那怎么办?及时解注册。在onDestroy里把监听器置空,或者使用Lifecycle感知组件自动管理。
总结一下:内存泄漏的本质是“长生命周期对象持有了短生命周期对象的引用”。只要抓住这个本质,你就能自己判断哪些代码有风险。
4.4 知识体系总览
下面这张图是我自己梳理的内存优化知识体系,帮你把今天讲的内容串起来:
好了,这一章的内容就到这里。内存优化是个系统工程,今天讲的这些场景,你回去翻翻自己的项目代码,大概率能找到几个。别慌,发现问题就是解决问题的第一步。
最后送大家一句话:内存泄漏不可怕,可怕的是你不知道它发生了。学会用Profiler、LeakCanary这些工具,让泄漏无处遁形。