核心是确认内部类是否被长生命周期对象持有及该引用链是否阻断外部类回收:一查字节码看有无this$0字段;二查线程池、静态容器、handler等持有方;三用heap dump追gc roots路径;四通过jstat监控gc行为初筛。

排查内部类持有外部类引用导致的内存泄漏,核心是确认“内部类是否被长生命周期对象持有着”,以及“这条引用链是否阻断了外部类的回收”。不需要等到服务崩溃,日常就能动手查。
一、看字节码:确认强引用是否真实存在
非静态内部类是否真的持有了外部类?最直接的办法是反编译字节码:
- 执行 javap -c OuterClass$InnerClass,查找是否有 final OuterClass this$0 字段;
- 如果有,说明编译器已生成强引用,内部类从诞生起就绑定了外部实例;
- 没有则大概率是静态内部类或 Lambda(且未捕获 this),基本可排除该路径泄漏。
二、查持有方:定位谁让内部类活得太久
内部类本身不危险,危险的是它被谁“养着”。重点检查以下几类持有者:
-
线程池:比如
executor.submit(new InnerClass()),任务未完成或未 cancel,内部类就一直存活; -
静态容器:如
static List<runnable> HOLDERS</runnable>缓存了内部类实例; - Handler / TimerTask / EventBus / RxJava Disposable:注册后未及时移除,内部类随这些组件长期驻留;
- View.postDelayed() 或 Activity 的匿名 Runnable:在 Android 场景中尤为典型,Activity 销毁后 Runnable 仍可能在消息队列中等待执行。
三、追 GC Roots:用 Heap Dump 实锤泄漏链
当怀疑某类对象堆积时,导出堆快照是最有力的证据:
- 用 jmap -dump:format=b,file=heap.hprof
获取 dump 文件; - 用 VisualVM、JProfiler 或 Eclipse MAT 打开,搜索疑似泄漏的外部类(如
MyService); - 右键该实例 → Merge Shortest Paths to GC Roots(排除弱/软引用);
- 若路径中出现
MyService$InnerClass → this$0 → MyService,且中间穿插ThreadPoolExecutor$Worker或static field,就是确凿证据。
四、运行期观察:结合监控快速初筛
不用等 dump,通过 JVM 运行指标也能发现苗头:
- 用 jstat -gc
1000 5 观察老年代使用率是否持续上涨、Full GC 后不回落; - 监控中看到 GC overhead limit exceeded 频繁出现,且重启后几小时内复现;
- Heap 使用曲线呈「阶梯式上升」,每次 GC 后都比前一次基线高——说明有对象始终无法释放。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











