Android Memory Profiler:堆内存指标、内存抖动与泄漏定位
Android Memory Profiler:堆内存指标、内存抖动与泄漏定位
建议先读:先理解第 3 节的四个堆内存指标,再进入第 4 节观察内存曲线、分配调用栈和引用链。
1. 阅读前问题卡:Memory Profiler 内存分析
1.1 阅读前先看这几个问题
Native Size、Shallow Size、Retained Size分别统计哪一部分内存?Depth表示什么,为什么Depth为 1 的实例尤其值得警惕?- 内存抖动和内存泄漏在
Memory Profiler中分别呈现什么特征? - 如何通过
Allocations、Instance View和Allocation Call Stack定位频繁创建对象的代码位置? - 为什么录制内存分配或生成 Heap Dump 前通常要先手动执行 GC?
- Heap Dump 中仍存在多个本应销毁的
Activity实例时,如何借助Reference判断是否发生泄漏?
1.2 读完后完成这 3 道高频问题
1.2.1 高频问题 1:请说明 Native Size、Shallow Size、Retained Size 和 Depth 的含义及它们在分析对象内存时的关系。
1.2.2 高频问题 2:如何使用 Memory Profiler 的内存曲线、对象分配数量和分配调用栈定位内存抖动?
1.2.3 高频问题 3:内存泄漏在 Memory Profiler 中有哪些表现,如何通过 Heap Dump、实例列表和引用链确认泄漏位置?
1.3 自检清单
- 能区分
Native Size、Shallow Size和Retained Size的统计范围。 - 能解释对象的
Depth与 GC Root 最短引用路径之间的关系。 - 能根据内存曲线区分频繁 GC、阶梯式增长和正常波动。
- 能复述从录制内存到定位分配调用栈的完整步骤。
- 能说明为什么 Heap Dump 前要先 GC,以及怎样判断多个
Activity实例是否异常存活。 - 能沿
Reference展示的引用链说明对象为什么没有被回收。
2. 前言
2.1 看清一个对象占用和牵连的内存
搬家时,一件家具本身占多少空间、它附带的包装占多少空间,以及移走它后能一并腾出多少空间,是三个不同的问题。还可以继续追问:从仓库出口到这件家具,最短要经过几道门。
在堆内存分析中,这几类观察分别对应对象本身的 Shallow Size、对象引用的原生对象所占的 Native Size、删除对象后可随之回收的 Retained Size,以及从 GC Root 到实例最短路径的 Depth。这些指标放在一起,才能同时看清单个对象的直接占用、关联占用和存活关系。
2.2 从波动和分配记录定位内存抖动
复印室里有人不断打印又丢弃大批草稿,纸张库存可能没有持续增长,但补纸和清理的动作会异常频繁。要找到源头,不能只看库存总量,还要查哪类草稿最多、是谁在什么时间发起了打印。
对应到 Memory Profiler,短时间内频繁创建对象会带来频繁 GC。先录制内存分配,再按 Allocations 查看数量较多的对象,选择具体实例并检查 Allocation Call Stack,就能把曲线上的异常活动落到实际的对象类型和创建位置。
2.3 沿引用链确认内存泄漏
与反复打印不同,仓库泄漏更像一批本该清走的旧物仍被登记在长期保管清单里。库存会一层层增加;要确认问题,需要查出是哪条保管关系让旧物一直不能离场。
在内存泄漏分析中,阶梯式上升且不回落的内存曲线是观察线索。Heap Dump 中经 GC 后仍然存在的多个 Activity 实例提供进一步证据,而实例下方的 Reference 则用于追踪保留这些实例的引用链,从而定位对象无法回收的原因。
3. Native Size、Shallow Size、Retained Size 与 Depth
后续说明 Memory Profiler 和 MAT 时,会经常出现几个比较重要的指标:Shallow Size 和 Retained Size。在 Memory Profiler 中还会提供 Native Size 和 Depth。Google 在“使用 Android Studio Profiler 工具解析应用的内存和 CPU 使用数据”中讲解了这几个指标的概念,下面会引用原文说明。Java 文档也对 Shallow Size 和 Retained Size 做了详细说明。
当您拿到一段 Heap Dump 之后,Memory Profiler 会展示类的列表。对于每个类,Allocations 一列显示它的实例数量,右边依次是 Native Size、Shallow Size 和 Retained Size:

我们用下图表示某段 Heap Dump 记录的应用内存状态。注意红色节点:在这个示例中,该节点所代表的工程对象引用了 Native 对象。这种情况不太常见,但在 Android 8.0 之后,使用 Bitmap 便可能产生此类情景,因为 Bitmap 会把像素信息存储在原生内存中,以减少 JVM 的内存压力。
先从 Shallow Size 讲起。这列数据就是对象本身消耗的内存大小,即红色节点自身所占的内存:

Native Size 是类对象所引用的 Native 对象(蓝色节点)消耗的内存大小:

Retained Size 稍复杂些,它是下图中所有橙色节点的大小:

一旦删除红色节点,其余橙色节点都将无法被访问,这时它们就会被 GC 回收。从这个角度讲,它们由红色节点持有,因此被命名为 Retained Size。
还有一个前面没有提到的数据维度。点击某个类名后,界面中会显示这个类的实例列表,其中有一列新数据:Depth。

Depth 是从 GC Root 到达这个实例的最短路径,图中的数字就是每个对象的深度。
一个对象离 GC Root 越近,就越有可能与 GC Root 通过多条路径相连,也越可能在垃圾回收中被保留下来。
以红色节点为例,如果从其左边来的任意一个引用被破坏,红色节点就会变成不可访问状态并被垃圾回收。对于右边的蓝色节点,如果希望它被垃圾回收,则需要把左右两边的路径都破坏。
如果看到某个实例的 Depth 为 1,就需要格外警惕:这意味着它直接被 GC Root 引用,同时也意味着它永远不会被自动回收。
下面是一个示例 Activity,它实现了 LocationListener 接口。高亮代码 requestLocationUpdates() 会使用当前 Activity 实例向 locationManager 注册监听。如果忘记注销,这个 Activity 就会泄漏。它将一直留在内存里,因为位置管理器是一个始终存在的 GC Root:

您可以在 Memory Profiler 中查看这一情况。点击一个实例,Memory Profiler 会打开面板,显示谁正在引用这个实例:

可以看到,位置管理器中的 mListener 正在引用这个 Activity。还可以通过引用面板导航到堆的引用视图,验证这条引用链是否符合预期,并借此判断代码中是否存在泄漏以及泄漏的位置。
4. Memory Profiler
Memory Profiler 是 Android Studio 内置的内存分析工具,适用于查看实时内存情况。
4.1 Memory Profiler 界面说明
官方文档:使用 Memory Profiler 查看 Java 堆和内存分配。
4.2 Memory Profiler 查找内存抖动
查找内存抖动比较简单。运行中的程序在 Memory Profiler 中会呈现为短时间内内存上下波动,并频繁触发 GC 回收。
内存抖动比较常见的地方:
- 自定义
View的onMeasure()、onLayout()、onDraw()中直接使用new创建对象 - 列表(如
RecyclerView)的onBindViewHolder()中直接使用new创建对象 - 有循环的代码中创建对象
用一个简单案例模拟内存抖动:
public class MainActivity extends AppCompatActivity {
@SuppressWarnings("HandlerLeak")
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 模拟内存抖动
for (int i = 0; i < 100; i++) {
String[] args = new String[100000];
}
mHandler.sendEmptyMessageDelayed(0, 30);
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mHandler.sendEmptyMessage(0);
}
});
}
}
这个案例就是点击按钮时频繁创建对象。在真机上运行上面的程序也许不会出现锯齿状的内存波动,但会有非常频繁的 GC 回收,如下图:

如何具体定位发生内存抖动的位置?

按照上图步骤操作:
- 位置①:程序运行时,点击
Record按钮录制内存情况,再点击Stop停止录制,界面会显示上图内容。 - 位置②:点击
Allocations,按降序或升序查看分配对象的数量。一般选择降序,优先查看数量最多的对象。上图中数量最多的是String对象。 - 位置③:在
Instance View中选择一个String对象,界面下方会显示Allocation Call Stack,其中包含该对象的调用栈位置。 - 位置④:从
Allocation Call Stack可以看到,String对象是在MainActivity第 18 行的handleMessage()中创建的,由此定位到内存抖动的位置。
上述操作还有一些小技巧:
- 执行位置①的操作前,为排除干扰,一般先手动执行 GC,再录制变化的内存。在 Android 8.0 以上的设备中,可以实时拖动
Memory Profiler,选择要查看的内存波动范围。 - 位置②的示例直接使用
Arrange by class查看,但在实际项目中,更多会选择Arrange by package,查看自己项目包名下的类。
4.3 Memory Profiler 查找内存泄漏
上面讲到,内存泄漏会伴随内存抖动。发生内存泄漏时,可用内存不断减少;系统需要内存却发现内存不足时就会执行 GC,因此产生内存抖动。
发生内存泄漏时,Memory Profiler 会呈现类似阶梯式的内存上升趋势,而且内存没有降下来:

上图的内存泄漏比较明显。实际项目开发中出现内存泄漏时,趋势可能并不明显,需要运行较长时间才能发现内存在缓慢上升。这时就需要 Dump Heap 帮助定位。
接下来使用 Handler 内存泄漏案例,简单说明如何使用 Memory Profiler 分析内存泄漏。
public class HandlerLeakActivity extends AppCompatActivity {
private static final String TAG = HandlerLeakActivity.class.getSimpleName();
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == 0) {
Log.i(TAG, "handler receive msg");
}
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handler.sendEmptyMessageDelayed(0, 10 * 1000);
}
}
上面的代码很简单:启动 App 后,每次进入 HandlerLeakActivity 都使用 Handler 延迟 10 秒发送消息;在 10 秒内退出界面,并不断重复该操作。
- 重复多次可能引发内存泄漏的操作,使用
Memory Profiler执行堆转储,生成 HPROF 文件。建议操作前先执行 GC,以排除干扰:
- 在
Memory Profiler中查看堆转储生成的 HPROF 文件:
可以发现,手动执行 GC 后,Allocations 中仍显示 5 个 HandlerLeakActivity,堆转储的 Instance View 下也仍显示多个 Activity 实例,说明已经发生内存泄漏。要进一步定位泄漏,可以在 Instance View 中点击发生泄漏的实例类对象;Instance View 下方的 Reference 会显示具体的引用链。
新版本的 Memory Profiler 提供了 Activity/Fragment Leaks 复选框,选中后可以直接找到可能发生内存泄漏的位置:

更多推荐
所有评论(0)