匿名内部类导致内存泄漏:一段代码引发的追问链
从 BigService 的 new Runnable() 出发,通过一问一答的形式,一步步拆解 this$0 导致外部类无法回收的完整链路。按真实学习过程还原。
匿名内部类导致内存泄漏:一段代码引发的追问链
起点:一段代码
先看这段代码。它就是整个问题的出发点。
public class BigService {
private byte[] bigData = new byte[100 * 1024 * 1024]; // 100MB
public Runnable getTask() {
// 匿名内部类,持有 BigService.this 的引用
return new Runnable() {
public void run() {
System.out.println("hello");
}
};
}
}
这段代码有一个隐蔽的内存泄漏问题。泄漏出在 new Runnable() { ... } 这个匿名内部类上。
下面通过一问一答的形式,还原当时的提问过程。全程按一个学习者的真实追问链路走。
第一组问答:先搞清楚这条引用链
先把代码跑起来,看看这条引用链怎么走的。
public class TaskRunner {
private Runnable task;
public void setTask(Runnable task) {
this.task = task;
}
}
这样使用:
TaskRunner runner = new TaskRunner();
BigService service = new BigService();
runner.setTask(service.getTask());
Q:runner.setTask(service.getTask()) 之后,runner 中的 task 属性指向什么?
A:指向那个 new Runnable() 对象。这一步没问题。
Q:那 Runnable 对象里有什么?
A:表面上看只有一个 run() 方法打印 hello。但 Java 编译器在你不知道的情况下,给这个匿名内部类自动加了一个属性。
Q:加了一个什么属性?
A:属性名叫 this$0,类型是 BigService。编译器编译完的匿名类,实际是这样的:
// 编译器生成的实际类,你对源码看不到这个,但 class 文件里就是这个结构
public class BigService$1 implements Runnable {
BigService this$0; // 编译器自动生成,类型是 BigService
BigService$1(BigService outer) {
this.this$0 = outer;
}
public void run() {
System.out.println("hello");
}
}
Q:所以内部类会自动生成一个属性,类型为外部类?
A:对。
Q:那这个 this$0 指向谁?
A:你在 BigService 里写 return new Runnable() { ... },编译器把它当成:
return new BigService$1(this); // this 就是那个 BigService 实例
所以 Runnable 对象中的属性 this$0 指向外部类对象。
画一下现在的内存图:
runner
│
└→ TaskRunner
│
└→ task ──→ Runnable 对象(打印 hello 那个)
│
└→ this$0 ──→ BigService 对象(带 100MB bigData)
这就是当前所有引用关系。
第二组问答:service = null 之后发生了什么?
Q:现在执行 service = null,你觉得谁会回收?
service = null;
A:先想清楚。栈上那个叫 service 的变量现在指向 null 了,它指向 BigService 的引用断了。
但看上面的内存图——runner.task 还指向 Runnable 对象,Runnable 对象还活着,Runnable 里面的 this$0 还拽着 BigService 对象。
Q:所以即使 service = null,但是 this$0 还在引用外部类对象,导致无法回收?
A:对。那 100MB 的 bigData 也就一直占着堆,释放不了。
Q:这就是内存泄漏吗?
A:对。内存泄漏的定义是:一个对象不再被程序使用了(你明确 service = null 表示不用它了),但因为还有引用指向它,GC 觉得它还活着,就不回收它。这 100MB 就是泄漏的内存。
Q:所以这个问题的核心是什么?
A:内部类会自动生成一个属性,属性类型为外部类,然后这个属性会指向外部类对象,导致外部类及其所有属性无法被 GC 回收,从而造成内存泄漏。
这段代码和这个内存图,必须同时存在
runner
│
└→ TaskRunner
│
└→ task ──→ Runnable 对象(打印 hello 那个)
│
└→ this$0 ──→ BigService 对象(带 100MB bigData)
↑
│
service = null 之后,
栈上指向 BigService 的变量没了,
但是 Runnable 还在指着它!
这个图就是整条链路的关键。看懂了这张图,就看懂了泄漏怎么发生的。
怎么修
方式一:改成静态内部类或 Lambda
public static Runnable getTask() {
return () -> System.out.println("hello");
}
静态上下文没有外部类实例,编译器不会生成 this$0 字段。Lambda 也一样——不会持有外部引用,除非你在 Lambda 里用到了外部类的字段。
方式二:用完断开引用链
runner.setTask(null);
Runnable 变成垃圾,this$0 跟着消失,BigService 就能回收了。
整条追问链
- 有一个 TaskRunner
- 先创建外部类实例(BigService)
- runner.setTask(service.getTask()) → runner 中的 task 属性指向一个 new Runnable 对象
- Runnable 对象中的属性 this$0 指向外部类对象
- 即使 service = null,但是 this$0 还在引用外部类对象,导致无法回收
- 所以核心是:内部类会自动生成一个属性,属性类型为外部类,这个属性指向外部类对象,导致外部类的属性(bigData)的内存无法回收
非静态内部类永远记着:你只 new 了一个小对象,但它背后偷偷拽着它爹不撒手。你爹要是内存很大,你不放,它就不走。