Skip to content

Android / Java 内存泄漏定位、LeakCanary 原理与 Profiler 实战 ​

回到总览:内存、生命周期、系统约束
相关模块:JVM / ART / Dart / Flutter 内存模型总览 · Android 生命周期总览 · JVM / ART 垃圾回收算法与 GC 演进深度剖析 · Java / Android 引用类型与 Dart 弱引用对照

一句话定义 ​

内存泄漏(Memory Leak)是指本来已经不再需要的对象仍然被某条不应该存在的强引用链持有,导致 GC 判定它仍然从 GC Roots 可达。它通常表现为托管对象无法回收,但进程整体内存问题还可能来自 Native heap、Bitmap、GPU 或外部资源,不能把所有 OOM 都直接归因于 Java 堆泄漏。

代码索引 ​

计划补齐的实验:

  • labs/android/host/topics/memory-lifecycle/memory-leak-demo/ — 常见内存泄漏场景重现、LeakCanary 监控与 Memory Profiler 堆转储分析

为什么需要 ​

  • 为什么 Activity 被销毁(onDestroy 已调用)后,它占用的内存不一定会立即被 GC 回收?
    • 一句话答:onDestroy() 只是系统给 Activity 的生命周期回调通知,如果在静态变量、单例或异步子线程中依然保留了对该 Activity (或其 Context) 的强引用,GC Roots 依然可达,它就会成为泄漏在堆中的“僵尸对象”。(详见下文 §1及复习题 2)
  • 为什么 10 年 Android 工程师必须掌握 LeakCanary 与 Profiler 原理?
    • 一句话答:线上 OOM 崩溃日志通常只显示内存分配失败的“受害现场”,真正原因还需要结合托管堆、Native/Bitmap/GPU 统计、引用链和分配记录判断;LeakCanary 与 Hprof/Heap Dump 能帮助定位托管对象的持有链,但不能单独覆盖所有进程内存问题。(详见下文 §2、§3及复习题 1)

底层机制 ​

1. 常见内存泄漏五大经典场景 ​

text
[长生命周期对象 (如 GC Root: Static / Singleton / Thread)]
       | (持有强引用)
       v
[短生命周期对象 (如 Activity / Fragment / View)] ---> [携带大量字节的 Bitmap / Cache]
  1. 单例模式 / 静态变量持有 Context:单例持有 Activity 的 Context,导致 Activity 无法随 View 销毁。
  2. 非静态内部类 / 匿名内部类隐式持有外部类引用:
    • 典型的 Handler 匿名内部类发延迟消息:Message 进了 MessageQueue,Message 的 target 指向 Handler,Handler 隐式持有 Activity!
  3. 未注销的监听器 / 观察者模式:EventBus.register()、RxJava 订阅、系统广播未在 onDestroy 调用 unregister()。
  4. 属性动画未在 View 销毁时停止:ValueAnimator 设置了 INFINITE 无限循环且持有 View 引用。
  5. WebView 资源未收口:WebView 可能通过渲染、JS、回调和 Context 持有较长生命周期资源,需要在合适的生命周期中停止加载、解除回调并调用 destroy();是否使用独立进程要根据隔离、稳定性和内存风险单独评估,不是通用泄漏修复方案。

判断一次“内存没降”到底是不是泄漏,至少要分三步:

text
页面/任务结束
   ↓
对象是否仍被 GC Root 强引用?
   ├─ 是:先追引用链,可能是泄漏或业务缓存
   └─ 否:对象已不可达,等待 GC,不一定是泄漏
          ↓
进程内存仍高?
   ├─ 托管堆高:看分配、晋升、缓存和 GC
   ├─ Native/Bitmap 高:看解码、JNI、FFI、插件资源
   └─ GPU/图形高:看纹理、Surface、BufferQueue、渲染缓存

onDestroy()、dispose() 和“对象不可达”是三个不同事件。页面回调结束只能说明生命周期进入某个阶段;泄漏判定还需要等待对象在合理时间后仍未被回收,并找到实际的强引用路径。

2. LeakCanary 工作原理深度拆解 ​

LeakCanary 是 Android 社区最成熟的内存泄漏自动化检测库,其底层核心机制分为 4 步:

text
1. 监听生命周期
   ActivityLifecycleCallbacks -> 捕获 onDestroy(activity)
          |
          v
2. 弱引用 + 引用队列 (WeakReference + ReferenceQueue)
   用 KeyedWeakReference 包装 activity,并关联 ReferenceQueue
          |
          v
   3. 延迟一段时间后请求 GC 检查(以下为原理化简化流程)
   Background Handler -> request GC
   判断 ReferenceQueue 中是否存在该 key:
     - 若存在:本次观察中该弱引用已进入队列,说明目标不再被强引用保持
     - 若不存在:可能仍有强引用,也可能只是 GC 尚未完成,需要继续观察和分析
          |
          v
4. Dump Hprof 并用 Shark 解析引用链
   生成 .hprof 文件 -> Shark 引擎分析 GC Roots -> 最短强引用路径 (Shortest Path to GC Roots) -> 报出泄漏!

弱引用 + 引用队列 (ReferenceQueue) 技巧代码 ​

java
// LeakCanary 核心逻辑简化
ReferenceQueue<Object> queue = new ReferenceQueue<>();
String key = UUID.randomUUID().toString();
// 用弱引用包裹被销毁的 Activity,并绑定 queue
WeakReference<Object> ref = new WeakReference<>(activity, queue);

// 触发 GC 后判断
if (queue.poll() == null) {
    // 未入队不等于确定泄漏:GC 可能尚未完成,也可能仍存在强引用
    analyzeHeapDump();
}

Android / Flutter / Web / Backend 对照 ​

维Android (Java/Kotlin)Flutter (Dart VM)Web / Frontend (JS)
内存泄漏根源内部类隐式持有外部类、单例/静态变量闭包持有 BuildContext、未 cancel 的 Stream全局变量、闭包/DOM 引用未释放
检测手段LeakCanary / Android ProfilerDevTools Memory Timeline / Heap SnapshotChrome DevTools Memory Heap Snapshot
主要危害引发 OOM 直接 Crash 闪退引起 UI 卡顿、内存报警页面变慢、浏览器 Tab 崩溃
防范工具WeakReference / LifecycleOwner 绑定dispose() 取消订阅、避免闭包强持有、DevTools Memory页面 Unmount 显式解绑 Listener

常见场景与代码修法 ​

1. 修复 Handler 导致的 Activity 内存泄漏 ​

错误示范(匿名内部类) ​

java
public class LeakyActivity extends Activity {
    // 隐式持有 LeakyActivity.this 强引用!
    private final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // ...
        }
    };
}

正确修复(静态内部类 + 弱引用) ​

java
public class SafeActivity extends Activity {
    private static class SafeHandler extends Handler {
        private final WeakReference<SafeActivity> mActivityRef;

        public SafeHandler(SafeActivity activity) {
            mActivityRef = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            SafeActivity activity = mActivityRef.get();
            if (activity != null && !activity.isFinishing()) {
                // 安全地处理 UI 逻辑
            }
        }
    }
    private final SafeHandler mHandler = new SafeHandler(this);

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null); // 清空队列中所有未执行的消息
    }
}

常见误配、事故后果与排障 ​

1. 利用 Android Studio Memory Profiler 抓取 Hprof 堆转储 ​

当怀疑有泄漏时:

  1. 启动 Android Profiler → Memory 仪表盘。
  2. 点击 Record Heap Dump 录制当前内存快照。
  3. 过滤搜索被销毁的 Activity 实例,查看 Depth 与 Instance Count;onDestroy 后短时间仍存在只能作为线索,最终要结合保留时间和 nearest GC root 判断是否泄漏。
  4. 右侧展开 References 面板,勾选 Show nearest GC root,定位是哪条强引用链阻止了 GC。

与相近概念对比 ​

概念发生原因表现结果治理方法
内存泄漏 (Memory Leak)垃圾对象从 GC Roots 依然强引用可达,无法被回收内存占用随使用持续攀升LeakCanary 找出引用链,切断强引用
内存抖动 (Memory Churn)短时间内频繁创建并销毁大量临时对象内存图呈锯齿状,频繁 GC 引起卡顿对象池复用、避免在 onDraw/循环中 new
内存溢出 (OOM)堆空间不足以分配新对象,且无法释放出足够空间直接抛出 OutOfMemoryError 导致 Crash优化 Bitmap 尺寸、修复泄漏、开大堆

Flutter 侧的泄漏与排障边界 ​

Flutter 没有 Android Activity 那样的单一“页面对象泄漏”入口,但页面级资源仍可能被异步源、缓存或 Native 资源持有:

资源常见持有者修法/验证
TextEditingController、AnimationControllerState、动画 ticker、输入系统在 dispose() 中释放,检查 State 是否仍被回调触达
StreamSubscription、Timer、ChangeNotifierStream、Timer、Provider/业务服务在页面或业务作用域结束时 cancel / dispose
闭包、Future 回调State、BuildContext、异步任务取消任务或让回调只更新仍存活的作用域;不要把弱引用当作生命周期治理替代品
ImageCache、大 Uint8List图片缓存、列表状态、业务缓存检查缓存上限、淘汰策略和是否重复持有原图与缩略图
FFI 指针、PlatformView、TextureNative 插件、Engine、GPU显式释放 Native 资源,结合 DevTools 和宿主侧工具观察

最小例子:

dart
class _DetailPageState extends State<DetailPage> {
  late final StreamSubscription<String> _subscription;

  @override
  void initState() {
    super.initState();
    _subscription = eventStream.listen((value) {
      if (!mounted) return;
      setState(() => _value = value);
    });
  }

  @override
  void dispose() {
    _subscription.cancel();
    super.dispose();
  }
}

这里的关键不是“加一个 mounted 就没有泄漏”,而是两件事同时成立:页面销毁后不再更新旧 State,并且订阅本身也被取消。mounted 主要防止晚到回调更新已销毁的 State,不能替代 cancel()。

对应实验 ​

计划补齐的实验:

  • labs/android/host/topics/memory-lifecycle/memory-leak-demo/ — 常见内存泄漏场景重现、LeakCanary 监控与 Memory Profiler 堆转储分析

复习检查题 ​

  1. LeakCanary 是如何巧妙判断一个已经被 onDestroy() 的 Activity 是否真的发生内存泄漏的?

    答:LeakCanary 会在 Activity 销毁后记录一个带 ReferenceQueue 的弱引用,并在等待和 GC 观察阶段检查对象是否仍被保留。弱引用进入队列说明本次观察中目标已不再被强引用保持;未进入队列只能说明对象仍可能存活或 GC 尚未完成,不能单独作为确定结论。随后 LeakCanary 会结合 retained object 的策略和 Shark 堆分析,寻找从 GC Roots 到目标对象的实际引用链。

  2. 为什么非静态内部类(包括匿名内部类)默认会持有外部类的强引用?

    答:在 Java/Kotlin 编译期,编译器会自动为非静态内部类的构造函数中添加一个隐式的外部类引用参数(即 this$0),使得内部类可以直接访问外部类的成员变量与方法。这种隐式强引用的生命周期如果比外部类长(如延迟 Handler 消息或异步子线程),就会阻止外部类被 GC 回收从而引发内存泄漏。

速记 ​

  • 泄漏本质:长生命周期强引用短生命周期对象,GC Roots 依然可达。
  • LeakCanary 原理:WeakReference + ReferenceQueue 负责发现疑似 retained object,再结合等待策略和 Heap Dump 引用链分析确认原因。
  • Handler 修复法则:静态内部类 + WeakReference 弱引用,onDestroy 清空消息队列。

站点构建时间:2026/8/24 23:43:17