Android 内存泄露实战 Profile,Leakcanary ,MAT
本文有2个例子, 通过3大工具分析,深入挖掘GC root,泄露的链路
1). 线程导致的内存泄漏
2). 自定义view导致的内存泄露 (前段时间自定义view系列)
1.案例:线程导致的内存泄漏
/**
* 演示非静态内部类导致内存泄漏的Activity示例
* 关键问题:MyThread作为非静态内部类隐式持有外部Activity的引用
*/
public class LeakThreadActivity extends Activity {
// 模拟Activity中占用内存的对象
private List<String> list = new ArrayList<String>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 模拟Activity中占用内存的操作
// 这里添加大量数据到list,使内存占用更明显(便于检测泄漏)
for(int i=0; i<10000;i++){
list.add("Memory Leak!"); // 添加测试数据
Log.d("LeakThreadActivity","Adding item "+i);
}
// 开启线程 - 这里会导致内存泄漏!
new MyThread().start(); // 启动后会持有Activity引用1分钟
}
/**
* 错误示例:非静态内部类线程
* 问题:
* 1. 非静态内部类默认持有外部类的隐式引用(this$0)
* 2. 当Activity销毁时,线程仍在运行(睡眠1分钟)
* 3. 导致Activity实例无法被GC回收
*/
public class MyThread extends Thread {
@Override
public void run() {
super.run();
// 模拟耗时操作(网络请求/文件IO等)
try {
Log.d("LeakThreadActivity","Thread sleep start");
Thread.sleep(1 * 60 * 1000); // 睡眠1分钟(模拟长耗时任务)
Log.d("LeakThreadActivity","Thread sleep end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d("LeakThreadActivity","onDestroy");
// 注意:这里没有中断线程,导致线程继续持有Activity引用
// 正确做法:应该在这里中断线程(但更好的方案是改用静态内部类)
}
}
2.泄漏的原因:
public class MyThread extends Thread { // 非静态内部类
@Override
public void run() {
Thread.sleep(60 * 1000); // 长耗时任务
}
}
内存泄露的原因:非静态内部类默认持有外部类的隐式引用(this$0) 关键问题:
- 隐式持有外部引用:非静态内部类默认持有外部类
LeakThreadActivity.this的强引用 - 生命周期不一致:线程存活时间(60秒) > Activity 生命周期(退出即销毁)
- 未中断线程:
onDestroy()中未终止线程,导致线程持续持有 Activity 引用
2. leakcanary分析内存泄露
2.1 leakcarnay的使用
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12' // 使用最新版本
2.2 leakcanary生成的报告
┬───
│ GC Root: Thread object (执行中的线程)
│
├─ com.example.LeakThreadActivity$MyThread instance
│ Leaking: UNKNOWN
│ ↓ MyThread.this$0
│ ~~~~~~
╰→ com.example.LeakThreadActivity instance
Leaking: YES (ObjectWatcher detected)
- GC Root:运行中的线程对象(Java 线程规则),GC Root对象是运行中的
MyThread线程实例 - 泄漏对象:
MyThread持有隐式引用this$0(指向外部 Activity)
2.3 leakcanary生成的hprof文件路径:
/storage/emulated/0/Download/leakcanary-com.evenbus.myapplication/2025-05-09_22-04-32_302.hprof
3. 修复方案
静态内部类 + 弱引用
静态内部类 + 弱引用能解决内存泄漏问题,是因为它切断了非必要的强引用链
private static class SafeThread extends Thread {
private final WeakReference<LeakThreadActivity> activityRef;
SafeThread(LeakThreadActivity activity) {
this.activityRef = new WeakReference<>(activity);
}
@Override
public void run() {
try {
Thread.sleep(60 * 1000);
LeakThreadActivity activity = activityRef.get();
if (activity != null) {
// 使用 activity 更新 UI
}
} catch (InterruptedException e) {
// 处理中断
}
}
}
// 启动线程
new SafeThread(this).start();
4.profile分析内存泄露的路径
4.1 选择heap Dump
4.2 会看到leaks有3个地方内存泄露,有3个LeakThreadActivity的实例,说明内存泄露了!(多个实例)
4.3 点击Reference,然后Reference Tree,选择show nearest GC root only!
4.4 再点击Jump to Source 可以查看代码
5.MAT分析内存泄露
5.1 导出profile抓取的文件.
5.2 转换hprof
- 工具路径:/platform-tools/hprof-conv
adb 没有配置环境变量: 需要用“”
C:\Users\pengc>"F:\Sdk\platform-tools\hprof-conv.exe" C:\Users\pengc\Desktop\memory-20250509T222810.hprof output.
MAT的安装: 需要jdk17
C:\Users\pengc.jdks\ms-17.0.14\bin
C:\Users\pengc.jdks\corretto-1.8.0_442
打开一个视图,就想打开一个网页一样,可以打开多个不同视图页面!
Top Consumers: 首页
以图形的形式展示内存,根据类名和包名列出开销最大的对象
我们看到下面有Histogram(直方图)他列举了每个对象的统计,Dominator Tree(支配树)提供了程序中最占内存的对象的排列,这两个是我在排查内存泄露的时候用的最多的
5.3 Histogram(直方图)
列出内存中的对象,对象的个数以及大小, Histogram是站在类的角度上去看,展示的是每个class对象的个数,大小等
步骤1: 我们可以通过正则表达式输入LeakActivity, 看到Histogram列出了与LeakActivity相关的类
在 Class Name 过滤框中输入你的 Activity 名称 (如 LeakThreadActivity)
步骤2:
查看对象数量 - 如果有多个实例则存在泄漏
步骤3:分析泄漏路径
- 右键点击泄漏的 Activity 实例
- 选择 →Merge Shortest Paths to GC Roots exclude weak/soft references
Merge Shortest Paths to GC Roots 可以查看一个对象到RC Roots是否存在引用链相连接,
(生成该对象到GC Roots最短路径)
在JAVA中是通过可达性(Reachability Analysis)来判断对象是否存活,这个算法的基本思想是通过一系列的称谓"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走得路径称为引用链,当一个对象到GC Roots没有任何引用链相连则该对象被判定为可以被回收的对象,反之不能被回收,
我们可以选择 exclude all phantom/weak/soft etc.references(排查虚引用/弱引用/软引用等)因为被虚引用/弱引用/软引用的对象可以直接被GC给回收.
步骤4:计算对象保留大小
右键对象 → Retained Set → Calculate Retained Size 可以精确计算泄漏的内存量
- Shallow Heap:对象自身占用的内存大小,不包括它引用的对象。如果是数组类型的对象,它的大小是数组元素的类型和数组长度决定。如果是非数组类型的对象,它的大小由其成员变量的数量和类型决定。
- Retained Heap:一个对象的Retained Set所包含对象所占内存的总大小。换句话说,Retained Heap就是当前对象被GC后,从Heap上总共能释放掉的内存。
5.4 Dominator Tree(支配树)
Dominator tree是站在对象(实例)的角度上去看,可以更方便的看出其引用关系。
主要看是否存在异常的大内存对象
步骤同上面的直方图是一样的!
5.4.1 按包名过滤:在顶部搜索框输入你的应用包名(如 com.example)
泄漏时:会显示多个残留实例 - 发现 1 个实例(正常应为 0)
5.4.2 观察 Retained Heap 列:
显示该对象及其引用链占用的总内存
用MAT分析,只有一个实例的情况下,是否内存泄漏? 是的
5.4.3 →Merge Shortest Paths to GC Roots exclude weak/soft references
在进行具体分析的时候MAT只是起了帮助你进行分析的工具的功能,OOM问题分析没有固定方法和准则。只能发挥你敏锐的洞察力,结合源代码,对内存中的对象进行分析从而找到代码中的BUG.
Leak Suspects:MAT自动检测的泄漏嫌疑点
这个是可疑的,不知道怎么用,和我实际的有区别!
关注"Problem Suspect 1"等部分
5.5 流程图
6.Histogram 对比功能(重点) 高级分析技巧
如果要进行对比,那么我们只需要点击这个就可以实现堆栈对比。对比两个文件,看看有没有内存泄漏。
如果是+,那么就是增加了多少,也就是有多少没有释放。 如果是+0,那么就是没有增加,也就是都释放了。
MAT的使用:
- 可以分不同维度来查看对象的Dominator Tree视图,Group by class、Group by class loader、Group by package
和Histogram类似,时间久了,通过多次对比也可以把溢出对象找出来。
除了使用Merge Shortest Paths to GC Roots 我们还可以使用
List object - With outgoing References 显示选中对象持有那些对象
List object - With incoming References 显示选中对象被那些外部对象所持有
Show object by class - With outgoing References 显示选中对象持有哪些对象, 这些对象按类合并在一起排序
Show object by class - With incoming References 显示选中对象被哪些外部对象持有, 这些对象按类合并在一起排序
Debug Bitmap
如果经常使用MAT分析内存,就会发现Bitmap所占用的内存是非常大的,这个和其实际显示面积是有关系的。在2K屏幕上,一张Bitmap能达到20MB的大小。
所以要是MAT提供了一种方法,可以将存储Bitmap的byte数组导出来,使用第三方工具打开。这个大大提高了我们分析内存泄露的效率。
MAT 中的直方图中的objects列代表什么意思?
在 Eclipse Memory Analyzer Tool (MAT) 的直方图(Histogram)视图中,Objects 列 表示 内存中该类的实例数量
MAT的引用关系链是怎么看的!
可以看到,只有一个强引用在占用,是由于一个第三方库的用法导致的
7. 案例二:为什么自定义view无法被回收
那就继续看,为什么自定义view无法被回收,可以看到,this$0,这表示在自定义view的内部有一个非静态内部类,而非静态内部类是默认持有外部类的引用的,也就是我们的,mNetChangeListener对象,这个就熟悉了吧,肯定是new 一个匿名内部类啊,
VideoPlayerActivity -> mContext -> CoverVideoPlayerView -> mNetChangeListener -> NetInfoModeule -> xxxxxBroadcastReceiver;
7.1 leakCanary的报告
根据LeakCanary的报告,这是一个典型的由非静态内部类和广播接收器导致的内存泄漏问题。我来分析这个引用链:
内存泄漏引用链(从GC Root到泄漏对象)
GC Root (System class)
│
├─ FontsContract.sContext (static)
│ ↓
├─ MatrixApplication (Application单例)
│ ↓ (mLoadedApk)
├─ LoadedApk instance
│ ↓ (mReceivers)
├─ ArrayMap (保存广播接收器)
│ ↓ (Object[1])
├─ ArrayMap (具体接收器映射)
│ ↓ (Object[2])
├─ NetInfoModule$NetworkBroadcastReceiver (内部类广播接收器)
│ ↓ (this$0) [关键点:非静态内部类隐式持有外部类引用]
├─ NetInfoModule instance
│ ↓ (listeners)
├─ ArrayList (监听器列表)
│ ↓ (elementData[0])
├─ CoverVideoPlayerView$1 (匿名内部类监听器)
│ ↓ (this$0) [关键点:匿名内部类隐式持有外部类引用]
├─ CoverVideoPlayerView (自定义View)
│ ↓ (mContext)
╰─ VideoPlayerActivity (已销毁的Activity) [泄漏点]
内存泄漏分析
这个例子中存在以下内存泄漏链:
- VideoPlayerActivity 持有 CoverVideoPlayerView 的引用
- CoverVideoPlayerView 持有 NetInfoModule 的引用
- NetInfoModule 持有 NetworkBroadcastReceiver 的引用
- NetworkBroadcastReceiver 是 NetInfoModule 的非静态内部类,隐式持有 NetInfoModule 的引用
- NetInfoModule 通过 mNetChangeListener 匿名内部类持有 CoverVideoPlayerView 的引用
- CoverVideoPlayerView 又持有 VideoPlayerActivity 的context引用
这样就形成了一个引用环,导致Activity无法被GC回收。
关键问题节点分析
1. NetworkBroadcastReceiver
- 这是一个非静态内部类,隐式持有外部类NetInfoModule的引用
- 被注册为系统广播接收器,成为长生命周期对象
2. CoverVideoPlayerView$1
- 匿名内部类实现的NetChangeListener
- 隐式持有外部类CoverVideoPlayerView的引用
3. CoverVideoPlayerView
- 持有了Activity的context引用 (mContext)
- 虽然View已detach,但被前面的引用链保持
引用链形成过程
- 系统广播服务持有NetworkBroadcastReceiver
- 接收器(非静态内部类)持有NetInfoModule
- NetInfoModule持有监听器列表
- 监听器(匿名内部类)持有CoverVideoPlayerView
- CoverVideoPlayerView持有Activity的context
- Activity销毁时,这一系列引用阻止了GC回收
泄漏的根本原因
- 非静态内部类:NetworkBroadcastReceiver作为非静态内部类,隐式持有外部类引用
- 匿名内部类:NetChangeListener作为匿名内部类,隐式持有CoverVideoPlayerView引用
- Context引用:自定义View直接持有了Activity context
- 生命周期不匹配:广播接收器是长生命周期的,而Activity是短生命周期的
7.2 profiler的分析图:
7.3 MAT分析
8.总结:
Profile分析和MAT内存泄漏,都是看有没有多个实例
项目的地址:github.com/pengcaihua1…