跳到正文
Joeplover
学习笔记·2026-07-11·约 4 分钟阅读

匿名内部类导致内存泄漏:一段代码引发的追问链

从 BigService 的 new Runnable() 出发,通过一问一答的形式,一步步拆解 this$0 导致外部类无法回收的完整链路。按真实学习过程还原。

Java 内存管理示意图,GC 工作机制

匿名内部类导致内存泄漏:一段代码引发的追问链

起点:一段代码

先看这段代码。它就是整个问题的出发点。

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 就能回收了。


整条追问链

  1. 有一个 TaskRunner
  2. 先创建外部类实例(BigService)
  3. runner.setTask(service.getTask()) → runner 中的 task 属性指向一个 new Runnable 对象
  4. Runnable 对象中的属性 this$0 指向外部类对象
  5. 即使 service = null,但是 this$0 还在引用外部类对象,导致无法回收
  6. 所以核心是:内部类会自动生成一个属性,属性类型为外部类,这个属性指向外部类对象,导致外部类的属性(bigData)的内存无法回收

非静态内部类永远记着:你只 new 了一个小对象,但它背后偷偷拽着它爹不撒手。你爹要是内存很大,你不放,它就不走。