Android Memory Profiler:堆内存指标、内存抖动与泄漏定位


Android Memory Profiler:堆内存指标、内存抖动与泄漏定位

  • [Android Memory Profiler:堆内存指标、内存抖动与泄漏定位](#Android Memory Profiler:堆内存指标、内存抖动与泄漏定位)
  • [1. 阅读前问题卡:Memory Profiler 内存分析](#1. 阅读前问题卡:Memory Profiler 内存分析)
    • [1.1 阅读前先看这几个问题](#1.1 阅读前先看这几个问题)
    • [1.2 读完后完成这 3 道高频问题](#1.2 读完后完成这 3 道高频问题)
    • [1.3 自检清单](#1.3 自检清单)
  • [2. 前言](#2. 前言)
    • [2.1 看清一个对象占用和牵连的内存](#2.1 看清一个对象占用和牵连的内存)
    • [2.2 从波动和分配记录定位内存抖动](#2.2 从波动和分配记录定位内存抖动)
    • [2.3 沿引用链确认内存泄漏](#2.3 沿引用链确认内存泄漏)
  • [3. Native Size、Shallow Size、Retained Size 与 Depth](#3. Native Size、Shallow Size、Retained Size 与 Depth)
  • [4. Memory Profiler](#4. Memory Profiler)
    • [4.1 Memory Profiler 界面说明](#4.1 Memory Profiler 界面说明)
    • [4.2 Memory Profiler 查找内存抖动](#4.2 Memory Profiler 查找内存抖动)
    • [4.3 Memory Profiler 查找内存泄漏](#4.3 Memory Profiler 查找内存泄漏)

建议先读:先理解第 3 节的四个堆内存指标,再进入第 4 节观察内存曲线、分配调用栈和引用链。


1. 阅读前问题卡:Memory Profiler 内存分析


1.1 阅读前先看这几个问题

  1. Native SizeShallow SizeRetained Size 分别统计哪一部分内存?
  2. Depth 表示什么,为什么 Depth 为 1 的实例尤其值得警惕?
  3. 内存抖动和内存泄漏在 Memory Profiler 中分别呈现什么特征?
  4. 如何通过 AllocationsInstance ViewAllocation Call Stack 定位频繁创建对象的代码位置?
  5. 为什么录制内存分配或生成 Heap Dump 前通常要先手动执行 GC?
  6. Heap Dump 中仍存在多个本应销毁的 Activity 实例时,如何借助 Reference 判断是否发生泄漏?

1.2 读完后完成这 3 道高频问题


1.2.1 高频问题 1:请说明 Native SizeShallow SizeRetained SizeDepth 的含义及它们在分析对象内存时的关系。


1.2.2 高频问题 2:如何使用 Memory Profiler 的内存曲线、对象分配数量和分配调用栈定位内存抖动?


1.2.3 高频问题 3:内存泄漏在 Memory Profiler 中有哪些表现,如何通过 Heap Dump、实例列表和引用链确认泄漏位置?


1.3 自检清单

  • 能区分 Native SizeShallow SizeRetained 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 SizeRetained Size。在 Memory Profiler 中还会提供 Native SizeDepth。Google 在"使用 Android Studio Profiler 工具解析应用的内存和 CPU 使用数据"中讲解了这几个指标的概念,下面会引用原文说明。Java 文档也对 Shallow SizeRetained Size 做了详细说明。

当您拿到一段 Heap Dump 之后,Memory Profiler 会展示类的列表。对于每个类,Allocations 一列显示它的实例数量,右边依次是 Native SizeShallow SizeRetained 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 回收。

内存抖动比较常见的地方:

  • 自定义 ViewonMeasure()onLayout()onDraw() 中直接使用 new 创建对象
  • 列表(如 RecyclerView)的 onBindViewHolder() 中直接使用 new 创建对象
  • 有循环的代码中创建对象

用一个简单案例模拟内存抖动:

java 复制代码
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 回收,如下图:

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

按照上图步骤操作:

  1. 位置①:程序运行时,点击 Record 按钮录制内存情况,再点击 Stop 停止录制,界面会显示上图内容。
  2. 位置②:点击 Allocations,按降序或升序查看分配对象的数量。一般选择降序,优先查看数量最多的对象。上图中数量最多的是 String 对象。
  3. 位置③:在 Instance View 中选择一个 String 对象,界面下方会显示 Allocation Call Stack,其中包含该对象的调用栈位置。
  4. 位置④:从 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 分析内存泄漏。

java 复制代码
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 秒内退出界面,并不断重复该操作。

  1. 重复多次可能引发内存泄漏的操作,使用 Memory Profiler 执行堆转储,生成 HPROF 文件。建议操作前先执行 GC,以排除干扰:
  2. Memory Profiler 中查看堆转储生成的 HPROF 文件:

可以发现,手动执行 GC 后,Allocations 中仍显示 5 个 HandlerLeakActivity,堆转储的 Instance View 下也仍显示多个 Activity 实例,说明已经发生内存泄漏。要进一步定位泄漏,可以在 Instance View 中点击发生泄漏的实例类对象;Instance View 下方的 Reference 会显示具体的引用链。

新版本的 Memory Profiler 提供了 Activity/Fragment Leaks 复选框,选中后可以直接找到可能发生内存泄漏的位置:

相关推荐
未来猫咪花17 分钟前
Everything is ViewModel:让状态管理回到对象世界
android·flutter·ios
古法安卓23 分钟前
Android-重启流程源码解析
android·java·android studio
协议的旁观者1 小时前
Android 高级逆向实战(一):对抗 360 企业加固,Native 抽取还原与 Activity 生命周期重建
android
2601_962387823 小时前
环境不稳、用例乱跳?Playwright自动化避坑大全
自动化测试·性能优化·playwright·测试环境·flakytests
mmsx4 小时前
osmdroid 屏幕坐标与测量坐标互转:中心点+比例尺仿射换算
android·源码·地图·osmdroid
00后程序员张4 小时前
Android证书绑定抓包失败?Android SSL Pinning绕过实战指南
android·网络协议·计算机网络·网络安全·adb·https·ssl
杉氧4 小时前
页面栈与路由:React Navigation 与 Expo Router 深度实践
android·前端·react native
XiaoLeisj4 小时前
Jetpack Compose 知识点
android·kotlin·android jetpack·compose
程序员贺加贝4 小时前
报表大 IN 优化:一条 product_profile 超大 IN SQL 背后的报表任务治理
算法·性能优化