云计算百科
云计算领域专业知识百科平台

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


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 阅读前先看这几个问题

  • 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)
    未经允许不得转载:网硕互联帮助中心 » Android Memory Profiler:堆内存指标、内存抖动与泄漏定位
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!