3. 内存泄漏原理:GC Roots、可达性分析算法、强/软/弱/虚引用
好,咱们来聊聊内存泄漏的根因。说白了,内存泄漏就是本该被回收的对象,因为某些原因一直「活着」。那JVM是怎么判断一个对象该不该回收的呢?这就得从GC Roots和可达性分析说起了。
3.1 GC Roots:垃圾回收的「起点」
GC Roots,你可以把它想象成一组「根对象」。JVM在回收内存时,会从这些根对象出发,沿着引用链往下找。凡是能被根对象直接或间接访问到的,就是「活的」;访问不到的,就是「死的」,可以被回收。
常见的GC Roots包括:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象:比如方法里的局部变量。
- 方法区中静态属性引用的对象:比如静态变量。
- 方法区中常量引用的对象:比如字符串常量池里的引用。
- 本地方法栈中JNI引用的对象:也就是Native方法引用的对象。
我个人习惯在分析内存泄漏时,先拿MAT(Memory Analyzer Tool)dump一份堆快照,然后直接看GC Roots的引用链。嗯,这招特别管用,能快速定位到谁「拽着」对象不放。
3.2 可达性分析算法:从根出发的「搜索」
可达性分析算法的核心思想很简单:从GC Roots出发,通过引用链向下搜索。搜索过程中经过的路径,叫「引用链」。如果一个对象到GC Roots没有任何引用链相连,那它就是不可达的,可以被回收。
举个例子:
// 假设有一个对象A,它被一个静态变量引用
static Object a = new Object();
// 那么A就是可达的,因为从GC Roots(静态变量)可以到达A
// 如果执行 a = null,A就变成不可达了,下次GC就会被回收
我在项目中遇到过一种情况:一个Activity被一个静态的Handler引用,导致Activity无法被回收。当时查了半天,最后发现是Handler里持有Activity的引用,而Handler本身是静态的。嗯,这就是典型的「非静态内部类持有外部类引用」导致的内存泄漏。
核心要点:只要对象从GC Roots可达,就不会被回收。内存泄漏的本质,就是让不该可达的对象「被可达」了。
3.3 强/软/弱/虚引用:引用的「四种强度」
Java里引用的强度分四种,从强到弱依次是:强引用、软引用、弱引用、虚引用。它们决定了对象被回收的「优先级」。
| 引用类型 | 回收时机 | 典型应用场景 |
|---|---|---|
| 强引用 | 永不回收(除非不可达) | 普通对象引用,如 new Object() |
| 软引用 | 内存不足时回收 | 缓存实现,如图片缓存 |
| 弱引用 | 下次GC时回收 | WeakHashMap、Handler防止泄漏 |
| 虚引用 | 任何时候都可能回收 | 对象回收跟踪,如DirectByteBuffer |
3.4 强引用:最常见的「罪魁祸首」
强引用就是我们平时写的 Object obj = new Object()。只要强引用还存在,GC就永远不会回收它。内存泄漏的绝大多数情况,都是因为强引用「拽着」对象不放。
举个例子:
// 强引用示例
public class MainActivity extends AppCompatActivity {
private static List<Object> leakList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 每次进入Activity,都往静态列表里加一个对象
leakList.add(new Object());
// 这个Object被静态变量leakList强引用,永远不会被回收
}
}
我曾经在一个项目里看到有人把Context存到了静态变量里,结果整个Activity树都泄漏了。嗯,这种问题排查起来特别头疼,因为泄漏的对象会「带」走一大片。
避坑指南:千万不要把Activity、Context等生命周期短的对象赋值给静态变量。如果非要存,记得在合适的时机置为null。
3.5 软引用:适合做「缓存」
软引用在内存充足时不会被回收,但一旦内存紧张,GC就会把软引用指向的对象干掉。所以它特别适合做缓存,比如图片缓存。
// 软引用示例
SoftReference<Bitmap> softRef = new SoftReference<>(bitmap);
// 使用前先检查是否已被回收
Bitmap cachedBitmap = softRef.get();
if (cachedBitmap != null) {
// 缓存命中
} else {
// 缓存已被回收,重新加载
}
我个人习惯用软引用做二级缓存,配合LruCache一起用。LruCache负责管理最近使用的对象,软引用负责兜底——万一内存不够了,软引用的对象会被自动回收,不会OOM。
3.6 弱引用:GC来了就「走」
弱引用比软引用更「弱」——只要发生GC,弱引用指向的对象就会被回收,不管内存够不够。它最常见的用途就是防止Handler导致的内存泄漏。
// 弱引用示例:防止Handler泄漏
public class MyActivity extends AppCompatActivity {
private MyHandler handler = new MyHandler(this);
private static class MyHandler extends Handler {
private WeakReference<MyActivity> activityRef;
public MyHandler(MyActivity activity) {
activityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MyActivity activity = activityRef.get();
if (activity != null) {
// 安全使用activity
}
}
}
}
你看,这里用WeakReference持有Activity,即使Handler还在处理消息,Activity也能被正常回收。嗯,这是Android开发里最经典的防泄漏写法。
小技巧:WeakHashMap也是基于弱引用实现的。如果你需要一个Map,但不想让key阻止GC回收,可以用WeakHashMap。
3.7 虚引用:最「弱」的引用
虚引用是最弱的引用,你甚至无法通过它获取到对象实例。它的唯一作用就是:当对象被回收时,系统会收到一个通知。说白了,它就是个「回收监听器」。
// 虚引用示例
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
// phantomRef.get() 永远返回null
// 当Object被回收时,phantomRef会被加入到queue中
虚引用在Android里用得不多,但在一些底层框架中很常见。比如Netty里就用虚引用来跟踪DirectByteBuffer的回收,确保堆外内存被正确释放。
3.8 总结:怎么用这些引用避免泄漏?
说了这么多,其实核心就一句话:用对引用类型,就能避免大部分内存泄漏。
- 强引用:用于常规对象,但注意不要「拽着」不该拽的对象。
- 软引用:适合做缓存,内存紧张时自动释放。
- 弱引用:适合做「临时关联」,比如Handler、观察者模式。
- 虚引用:适合做「回收通知」,比如资源释放监控。
我在做性能优化时,经常跟团队说一句话:「写代码时多想想这个对象的生命周期,别让它活得太久。」嗯,这句话虽然简单,但能解决80%的内存泄漏问题。
好了,这一章就到这里。下一章咱们聊聊怎么用工具定位内存泄漏,到时候我会分享一些实战中的「骚操作」。