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 Size、Shallow Size、Retained Size 分别统计哪一部分内存?
  2. Depth 表示什么,为什么 Depth 为 1 的实例尤其值得警惕?
  3. 内存抖动和内存泄漏在 Memory Profiler 中分别呈现什么特征?
  4. 如何通过 Allocations、Instance View 和 Allocation Call Stack 定位频繁创建对象的代码位置?
  5. 为什么录制内存分配或生成 Heap Dump 前通常要先手动执行 GC?
  6. 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 创建对象
  • 有循环的代码中创建对象

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

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 复选框,选中后可以直接找到可能发生内存泄漏的位置:

相关推荐
墨天梦41 分钟前
D09_Retrofit接口与数据转换
android·kotlin·retrofit
电商API_180079052472 小时前
闲鱼商品列表API接口商品详情API接口请求示例(含一份真实响应数据示例)
android·数据挖掘·网络爬虫·跨境电商·反向海淘
我命由我123452 小时前
Android 广播 - 组件通信
android·java·开发语言·android studio·android jetpack·android-studio·android runtime
阿俊-全栈开发2 小时前
LikeShop 单商户标准版商品管理二开:分类、SKU 规格与库存联动实现
android·likeshop·likeshop开源商城
木头科技5 小时前
【AI 工程化第十五篇】Spring AI 流式输出高并发性能优化实战:SSE、背压、限流、缓存与灰度发布
人工智能·spring·性能优化
用户2181697049305 小时前
Kotlin 语法二
android
打呵欠的猫5 小时前
用 AI 做性能分析:一行命令找出拖慢页面的 5 个组件,优化后首屏快了 40%
前端·性能优化·ai编程
Cx330❀5 小时前
Qt 界面美化指南:从零掌握 QSS 核心机制与工程实践
开发语言·qt·ui·性能优化·图形渲染
妖星5 小时前
android 源码分析surfaceFinger是如何通过设置属性启动bootanim
android