Android Memory Profiler:堆内存指标、内存抖动与泄漏定位
- Android Memory Profiler:堆内存指标、内存抖动与泄漏定位
- 1. 阅读前问题卡:Memory Profiler 内存分析
- 1.1 阅读前先看这几个问题
- 1.2 读完后完成这 3 道高频问题
- 1.3 自检清单
- 2. 前言
- 2.1 看清一个对象占用和牵连的内存
- 2.2 从波动和分配记录定位内存抖动
- 2.3 沿引用链确认内存泄漏
- 3. Native Size、Shallow Size、Retained Size 与 Depth
- 4. Memory Profiler
- 4.1 Memory Profiler 界面说明
- 4.2 Memory Profiler 查找内存抖动
- 4.3 Memory Profiler 查找内存泄漏
建议先读:先理解第 3 节的四个堆内存指标,再进入第 4 节观察内存曲线、分配调用栈和引用链。
1. 阅读前问题卡:Memory Profiler 内存分析
1.1 阅读前先看这几个问题
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 回收,如下图:

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

按照上图步骤操作:
上述操作还有一些小技巧:
- 执行位置①的操作前,为排除干扰,一般先手动执行 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 秒内退出界面,并不断重复该操作。


可以发现,手动执行 GC 后,Allocations 中仍显示 5 个 HandlerLeakActivity,堆转储的 Instance View 下也仍显示多个 Activity 实例,说明已经发生内存泄漏。要进一步定位泄漏,可以在 Instance View 中点击发生泄漏的实例类对象;Instance View 下方的 Reference 会显示具体的引用链。
新版本的 Memory Profiler 提供了 Activity/Fragment Leaks 复选框,选中后可以直接找到可能发生内存泄漏的位置:

网硕互联帮助中心





评论前必须登录!
注册