内部类的内存泄漏:Handler持有Activity是怎么让内存撑破的
内部类的内存泄漏:Handler持有Activity是怎么让内存撑破的
适读人群:Android开发者、Java后端开发者(服务端同样存在此类问题)| 阅读时长:约13分钟 | 文章类型:原理剖析+内存泄漏诊断
开篇故事
认识一个做Android的朋友,花名叫小鱼。有一次他跟我说,他们App有个奇怪的问题:用户反复进出某个图片列表页,内存会一直涨,最终OOM崩溃。
他把代码发我看:
public class ImageListActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 处理图片加载完成的消息
updateUI(msg.obj);
}
};
private void loadImages() {
// 开始异步加载,加载完成后发消息
imageLoader.load(urls, new Callback() {
@Override
public void onComplete(List<Bitmap> images) {
Message msg = mHandler.obtainMessage(1, images);
mHandler.sendMessageDelayed(msg, 0);
}
});
}
}我一看就知道问题在哪了:非静态内部类Handler隐式持有外部类Activity的引用。
当用户退出Activity时,Activity本该被GC回收。但只要mHandler在MessageQueue里还有未处理的消息,或者有任何地方持有mHandler的引用,GC就回收不了mHandler。而mHandler持有Activity,所以Activity也回收不了——哪怕用户已经离开了这个页面。
这不是Android特有的问题。Java后端代码里,用内部类做监听器、回调,一样会碰到这个问题。今天从根本原理说起。
一、非静态内部类的隐式引用
Java内部类的隐式引用机制
Java规定:非静态内部类(包括匿名内部类)会持有一个对外部类实例的隐式引用。
这个引用是编译器自动生成的,你在源码里看不到它,但它确确实实存在:
public class Outer {
private String name = "outer";
class Inner {
void printOuterName() {
System.out.println(name); // 访问外部类成员,通过隐式引用
}
}
}编译后,Inner类大致相当于:
class Outer$Inner {
private final Outer this$0; // 编译器自动加的,指向外部类实例
Outer$Inner(Outer outer) {
this.this$0 = outer; // 构造时传入外部类引用
}
void printOuterName() {
System.out.println(this$0.name); // 通过隐式引用访问
}
}内存泄漏的触发条件
内存泄漏的触发需要两个条件同时满足:
- 非静态内部类的实例(或匿名内部类的实例)生命周期比外部类实例更长
- 有某个长生命周期的对象持有内部类实例的引用
GC Root
|
└─> 长生命周期对象(线程池、静态Map、MessageQueue...)
|
└─> 内部类实例(Handler、Listener、Runnable...)
|
└─> [隐式引用] 外部类实例(Activity、Service...)
|
└─> Activity的所有资源(View、Bitmap...)
全部无法被GC回收!二、核心原理深挖
引用链分析
GC Root → 主线程 → Looper → MessageQueue → Message → Handler → Activity
这条引用链里,只要Handler还在MessageQueue里等着被处理(哪怕只是延迟几百毫秒),整条链上的所有对象都不能被GC。
后端服务中的类似场景
别以为这只是Android的问题。Java后端同样会有这类泄漏:
// 场景1:非静态内部类作为CompletableFuture回调
public class OrderService {
private final OrderRepository orderRepo;
public CompletableFuture<Void> processOrder(Order order) {
return CompletableFuture.runAsync(new Runnable() {
// 匿名Runnable隐式持有OrderService实例
@Override
public void run() {
// ...
}
}, executor);
}
}
// 场景2:非静态内部类注册到全局EventBus/监听器
public class UserManager {
public void init() {
EventBus.getDefault().register(new Object() {
@Subscribe
public void onEvent(UserEvent event) {
// 匿名类持有UserManager实例
// 如果没有unregister,UserManager永远不会被回收
}
});
}
}
// 场景3:定时任务中的匿名Runnable
public class DataSyncService {
private List<String> pendingData = new ArrayList<>();
public void startSync() {
scheduler.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
// 匿名Runnable持有DataSyncService实例
// 只要定时任务不停,DataSyncService就不能被GC
syncPendingData();
}
}, 0, 60, TimeUnit.SECONDS);
}
private void syncPendingData() { /* ... */ }
}三、完整代码实现
代码一:Android端Handler内存泄漏的正确修复
package com.laozhang.trap.innerclass;
import android.app.Activity;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import java.lang.ref.WeakReference;
/**
* Handler内存泄漏的正确修复方式
*
* 核心思路:
* 1. 将Handler改为静态内部类(静态内部类不持有外部类引用)
* 2. 用WeakReference持有Activity,需要访问Activity时先检查是否还存活
*/
public class ImageListActivity extends Activity {
private static final int MSG_IMAGES_LOADED = 1;
private static final int MSG_LOAD_ERROR = 2;
// 静态内部类:不持有外部Activity引用
private static class ImageHandler extends Handler {
// WeakReference:当Activity被销毁时,GC可以回收它,WeakReference返回null
private final WeakReference<ImageListActivity> activityRef;
ImageHandler(ImageListActivity activity) {
super(Looper.getMainLooper());
this.activityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
// 每次使用前,检查Activity是否还活着
ImageListActivity activity = activityRef.get();
if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
// Activity已经销毁,忽略消息
return;
}
switch (msg.what) {
case MSG_IMAGES_LOADED:
activity.updateUI(msg.obj);
break;
case MSG_LOAD_ERROR:
activity.showError((String) msg.obj);
break;
}
}
}
// 使用静态内部类Handler
private final ImageHandler mHandler = new ImageHandler(this);
private void loadImages() {
// imageLoader.load(urls, result -> {
// Message msg = mHandler.obtainMessage(MSG_IMAGES_LOADED, result);
// mHandler.sendMessage(msg);
// });
}
@Override
protected void onDestroy() {
super.onDestroy();
// 关键:Activity销毁时,移除所有pending消息
// 防止onDestroy之后消息还在队列里等待
mHandler.removeCallbacksAndMessages(null);
}
private void updateUI(Object data) { /* ... */ }
private void showError(String error) { /* ... */ }
}代码二:Java后端服务中的内存泄漏修复
package com.laozhang.trap.innerclass;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.*;
/**
* 后端服务中非静态内部类导致内存泄漏的修复
*/
public class BackendMemoryLeakFix {
// ===== 修复1:定时任务使用静态方法引用或外部类 =====
public static class DataSyncService {
private final List<String> pendingData = new ArrayList<>();
private final ScheduledExecutorService scheduler;
public DataSyncService() {
scheduler = Executors.newSingleThreadScheduledExecutor(
r -> new Thread(r, "data-sync-thread")
);
}
public void startSync() {
// 正确方式:用方法引用,等价于静态绑定
scheduler.scheduleAtFixedRate(this::syncPendingData, 0, 60, TimeUnit.SECONDS);
// 注意:this::syncPendingData 仍然持有this引用
// 但只要管理好scheduler的生命周期,shutdown时任务会停止
}
private void syncPendingData() {
System.out.println("同步数据,pending数量: " + pendingData.size());
}
// 关闭时必须shutdown scheduler
public void stop() {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
// ===== 修复2:事件监听器使用弱引用注册表 =====
public interface DataChangeListener {
void onDataChanged(String data);
}
/**
* 使用弱引用的监听器注册中心
* 当监听器的持有者被GC回收后,弱引用自动失效,不会泄漏
*/
public static class WeakListenerRegistry {
private final List<WeakReference<DataChangeListener>> listeners = new CopyOnWriteArrayList<>();
public void register(DataChangeListener listener) {
listeners.add(new WeakReference<>(listener));
}
public void unregister(DataChangeListener listener) {
listeners.removeIf(ref -> {
DataChangeListener l = ref.get();
return l == null || l == listener;
});
}
public void notifyAll(String data) {
Iterator<WeakReference<DataChangeListener>> it = listeners.iterator();
while (it.hasNext()) {
WeakReference<DataChangeListener> ref = it.next();
DataChangeListener listener = ref.get();
if (listener == null) {
// 监听器已被GC,移除弱引用
it.remove();
} else {
listener.onDataChanged(data);
}
}
}
}
// ===== 修复3:CompletableFuture回调使用静态内部类 =====
public static class OrderProcessor {
private final String processorName;
private final ExecutorService executor;
public OrderProcessor(String name) {
this.processorName = name;
this.executor = Executors.newFixedThreadPool(4);
}
// 错误写法:匿名Runnable持有OrderProcessor实例
@Deprecated
public CompletableFuture<String> processWrong(String orderId) {
return CompletableFuture.supplyAsync(() -> {
// lambda隐式捕获了this(OrderProcessor实例)
return "processed by " + processorName + ": " + orderId;
}, executor);
}
// 正确写法:只捕获需要的字段,而不是整个this
public CompletableFuture<String> processCorrect(String orderId) {
String name = this.processorName; // 只捕获值,不捕获this
return CompletableFuture.supplyAsync(() -> {
return "processed by " + name + ": " + orderId;
}, executor);
}
public void shutdown() {
executor.shutdown();
}
}
public static void main(String[] args) throws Exception {
// 测试弱引用监听器
WeakListenerRegistry registry = new WeakListenerRegistry();
// 注册一个监听器
DataChangeListener listener = data -> System.out.println("收到数据: " + data);
registry.register(listener);
registry.notifyAll("hello"); // 输出:收到数据: hello
// 如果listener被GC(这里手动置null并触发GC)
// 实际场景中是对象的持有者被销毁
listener = null;
System.gc();
Thread.sleep(100);
registry.notifyAll("world"); // 监听器已失效,不再收到通知
System.out.println("存活的监听器数量: " + registry.listeners.stream()
.filter(ref -> ref.get() != null).count());
}
}四、踩坑实录
坑1:Lambda表达式隐式捕获this
报错现象:
使用内存分析工具(MAT/VisualVM)发现大量Service对象无法被GC,heap dump里对象数量随时间单调递增。
触发代码:
public class NotificationService {
private final String serviceId = UUID.randomUUID().toString();
private final Map<String, List<Object>> subscribers;
public void subscribe(String topic, Consumer<String> handler) {
// lambda捕获了this(因为调用了this.serviceId)
subscribers.computeIfAbsent(topic, k -> new ArrayList<>())
.add((Consumer<String>) msg -> {
System.out.println(serviceId + " received: " + msg); // 访问了this.serviceId
handler.accept(msg);
});
}
// 如果subscribers是静态的或被其他长生命周期对象持有,
// NotificationService实例永远无法被GC
}根本原因:
Lambda表达式访问了外部类的实例字段,就会隐式捕获this。如果这个lambda被存到一个比外部类生命周期更长的容器里,就会发生泄漏。
具体解法:
public void subscribe(String topic, Consumer<String> handler) {
String id = this.serviceId; // 先把需要的值提取出来
subscribers.computeIfAbsent(topic, k -> new ArrayList<>())
.add((Consumer<String>) msg -> {
System.out.println(id + " received: " + msg); // 只捕获值,不捕获this
handler.accept(msg);
});
}坑2:ThreadLocal变量使用不当引发内存泄漏
这个坑跟内部类稍有不同,但都是"隐式持有"的问题,一并说了。
报错现象:
使用线程池的服务,内存随运行时间缓慢增长,没有OOM但内存占用持续升高。
根本原因:
private static final ThreadLocal<UserContext> CONTEXT = new ThreadLocal<>();
// 设置了ThreadLocal值
CONTEXT.set(new UserContext(userId, token));
// 处理完请求后,忘了remove
// 线程池里的线程是复用的,线程生命周期很长
// UserContext对象永远不会被GC具体解法:
// 使用try-finally确保一定remove
CONTEXT.set(new UserContext(userId, token));
try {
// 处理请求
doProcess();
} finally {
CONTEXT.remove(); // 必须清理
}坑3:匿名内部类存入静态集合
报错现象:
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.ArrayList.grow(ArrayList.java:265)
at com.example.EventBus.register(EventBus.java:78)触发代码:
public class HomeController {
@Autowired
private EventBus eventBus; // Spring单例,生命周期=整个应用
@PostConstruct
public void init() {
eventBus.register(new EventListener() { // 匿名内部类持有HomeController引用
@Override
public void onEvent(AppEvent event) {
handleEvent(event);
}
});
// 没有注销!每次Controller被Spring重新实例化(如热部署),
// 都会往EventBus里注册一个新的匿名监听器,且旧的没有移除
}
private void handleEvent(AppEvent event) { /* ... */ }
}具体解法:
@PreDestroy
public void destroy() {
eventBus.unregister(this.listener); // 销毁时注销
}五、总结与延伸
内部类的内存泄漏,核心是一句话:如果内部类实例的生命周期可能超过外部类,就不要用非静态内部类。
几个实用的判断标准:
会发生内存泄漏的情况:
- 非静态内部类/匿名类实例被存到静态变量或全局容器(EventBus、线程池任务队列)
- lambda捕获了this(或外部类字段,导致隐式捕获this)并被存到长生命周期对象
- 非静态内部类作为线程的Runnable
不会发生内存泄漏的情况:
- 内部类实例完全是局部的,用完即弃(如在方法内部的回调,方法执行完就没有引用了)
- 使用静态内部类
- 使用WeakReference持有外部类引用
工具推荐:
- VisualVM / JProfiler:运行时内存分析,可以看对象引用关系
- Eclipse MAT:分析heap dump,找GC Root到泄漏对象的引用路径
- Android Studio Memory Profiler:Android专用,可以实时看内存分配
内存泄漏是慢性病,不像OOM那样一眼能看出来。养成好的编码习惯比事后排查要省力得多。
