章节索引 :

Android Studio 如何分析内存活动

前面的小节我们学习了如何分析 CPU 活动。本小节学习如何分析内存活动。

1. 什么是 Memory Profiler

1.1 Memory Profiler 概览

Memory Profiler 是 Android Profiler 中的一个组件,可帮助我们识别可能会导致应用卡顿、冻结甚至崩溃的内存泄露和内存抖动。它显示一个应用内存使用量的实时图表,让我们可以捕获堆转储、强制执行垃圾回收以及跟踪内存分配。

如果我们的应用分配内存的速度比系统回收内存的速度快,则当回收器释放足够的内存以满足我们的分配需要时,我们的应用可能会延迟。此延迟可能会导致我们的应用跳帧,并使系统明显变慢。如果存在内存泄露,则即使应用在后台运行也会保留该内存。此行为会强制执行不必要的垃圾回收事件,因而拖慢系统其余部分的内存性能。

为帮助防止这些问题,我们应使用 Memory Profiler 执行以下操作:

  • 在时间轴上查找可能会导致性能问题的不理想的内存分配模式;

  • 转储 Java 堆以查看在任何给定时间哪些对象耗尽了内存。在很长一段时间内进行多次堆转储有助于识别内存泄露;

  • 记录正常用户交互和极端用户交互期间的内存分配,以准确识别我们的代码在何处短时间内分配了过多对象,或分配了泄露的对象。

1.2 打开 Memory Profiler

要打开 Memory Profiler,请按以下步骤操作:

依次点击 View > Tool Windows > Profiler,可以点击工具栏中的 Profile 图标。

Android Profiler 工具栏中选择要分析的设备和应用进程。

点击 MEMORY 时间轴上的任意位置以打开 Memory Profiler

当我们首次打开 Memory Profiler 时,我们将看到一条表示应用内存使用量的详细时间轴,并可使用各种工具来强制执行垃圾回收、捕获堆转储以及记录内存分配。

  1. 用于强制执行垃圾回收事件的按钮;

  2. 用于捕获堆转储的按钮;

  3. 用于指定分析器多久捕获一次内存分配的下拉菜单。选择适当的选项可帮助我们在分析时提高应用性能;

  4. 用于缩放时间轴的按钮;

  5. 用于跳转到实时内存数据的按钮;

  6. 事件时间轴,显示活动状态、用户输入事件和屏幕旋转事件;

  7. 内存使用量时间轴,它会显示以下内容:

    • 一个堆叠图表,显示每个内存类别当前使用多少内存,如左侧的 y 轴以及顶部的彩色键所示;

    • 一条虚线,表示分配的对象数,如右侧的 y 轴所示;

    • 每个垃圾回收事件的图标。

2. 如何计算内存

我们在 Memory Profiler 顶部看到的数字基于我们的应用根据 Android 系统机制所提交的所有私有内存页面。此计数不包含与系统或其他应用共享的页面。

内存计数中的类别如下:

  • Java:从 Java 或 Kotlin 代码分配的对象的内存;

  • Native:从 C 或 C++ 代码分配的对象的内存;

  • Graphics:图形缓冲区队列向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存;(请注意,这是与 CPU 共享的内存,不是 GPU 专用内存。)

  • Stack:我们的应用中的原生堆栈和 Java 堆栈使用的内存。通常与我们的应用运行多少线程有关;

  • Code:我们的应用用于处理代码和资源(如 dex 字节码、经过优化或编译的 dex 代码、.so 库和字体)的内存;

  • Others:我们的应用使用的系统不确定如何分类的内存;

  • Allocated:我们的应用分配的 Java/Kotlin 对象数。此数字没有计入 C 或 C++ 中分配的对象。

3. 内存分配

3.1 如何查看内存分配

内存分配为我们显示内存中的每个 Java 对象和 JNI 引用是如何分配的。具体而言,Memory Profiler 可为我们显示有关对象分配的以下信息:

  • 分配了哪些类型的对象以及它们使用多少空间;

  • 每个分配的堆栈轨迹,包括在哪个线程中;

  • 对象在何时被取消分配。

如果我们的设备搭载的是 Android 8.0 或更高版本,我们可以随时查看对象分配,具体操作步骤如下:在时间轴上拖动以选择要查看哪个区域的分配。不需要开始记录会话,因为 Android 8.0 及更高版本附带设备内置分析工具,可持续跟踪我们的应用分配。

如果我们的设备搭载的是 Android 7.1 或更低版本,请点击 Memory Profiler 工具栏中的 Record memory allocations 图标。记录时,Memory Profiler 会跟踪我们的应用中发生的所有分配。完成后,请点击 Stop recording 图标以查看分配。

3.2 检查分析分配记录

选择时间轴的某个区域后(或者使用搭载 Android 7.1 或更低版本的设备完成记录会话后),已分配对象的列表将显示在时间轴下方,按类名称进行分组,并按其堆计数排序。

要检查分配记录,请按以下步骤操作:

  1. 浏览列表以查找堆计数异常大且可能存在泄露的对象。为帮助查找已知类,点击 Class Name 列标题以按字母顺序排序。然后,点击一个类名称。此时右侧将出现 Instance View 窗格,显示该类的每个实例;

  2. Instance View 窗格中,点击一个实例。此时下方将出现 Call Stack 标签页,显示该实例被分配到何处以及在哪个线程中;

  3. Call Stack 标签页中,右键点击任意行并选择 Jump to Source,以在编辑器中打开该代码。

我们可以使用已分配对象列表上方的两个菜单来选择要检查的堆以及如何组织数据。从左侧的菜单中,选择要检查的堆:

  • default heap:当系统未指定堆时;

  • image heap:系统启动映像,包含启动期间预加载的类。此处的分配保证绝不会移动或消失;

  • zygote heap:写时复制堆,其中的应用进程是从 Android 系统中派生的;

  • app heap:我们的应用在其中分配内存的主堆;

  • JNI heap:显示 Java 原生接口 (JNI) 引用被分配和释放到什么位置的堆。

从右侧的菜单中,选择如何安排分配:

  • Arrange by class:根据类名称对所有分配进行分组。这是默认选项;

  • Arrange by package:根据软件包名称对所有分配进行分组;

  • Arrange by callstack:将所有分配分组到其对应的调用堆栈;

3.2 查看全局 JNI 引用

JNI 引用由原生代码进行管理,因此原生代码使用的 Java 对象可能会保持活动状态太长时间。如果丢弃了 JNI 引用而未先明确将其删除,Java 堆上的某些对象可能会变得无法访问。此外,还可能会达到全局 JNI 引用限制。

要排查此类问题,请使用 Memory Profiler 中的 JNI heap 视图来浏览所有全局 JNI 引用,并按 Java 类型和原生调用堆栈对其进行过滤。借助此信息,我们可以了解创建和删除全局 JNI 引用的时间和位置。

在我们的应用运行时,选择我们要检查的一部分时间轴,然后从类列表上方的下拉菜单中选择 JNI heap。 我们随后可以像往常一样检查堆中的对象,还可以双击 Allocation Call Stack 标签页中的对象,以查看在代码中将 JNI 引用分配和释放到了什么位置,如下图所示。

Tips:要检查应用的 JNI 代码的内存分配,必须将应用部署到搭载 Android 8.0 或更高版本的设备上。

4. 堆转储

堆转储显示在我们捕获堆转储时我们的应用中哪些对象正在使用内存。特别是在长时间的用户会话后,堆转储会显示我们认为不应再位于内存中却仍在内存中的对象,从而帮助识别内存泄露。

捕获堆转储后,我们可以查看以下信息:

  • 我们的应用分配了哪些类型的对象,以及每种对象有多少;

  • 每个对象当前使用多少内存;

  • 在代码中的什么位置保持着对每个对象的引用;

  • 对象所分配到的调用堆栈。

4.1 如何捕获堆转储

要捕获堆转储,请点击 Memory Profiler 工具栏中的 Dump Java heap 图标。 在转储堆期间,Java 内存量可能会暂时增加。 这很正常,因为堆转储与我们的应用发生在同一进程中,并需要一些内存来收集数据。

堆转储出现在内存时间轴下方,显示堆中的所有类类型,如下图所示。

在类列表中,我们可以查看以下信息:

  • Allocations:堆中的分配数;

  • Native Size:此对象类型使用的原生内存总量(以字节为单位)。只有在使用 Android 7.0 及更高版本时,才会看到此列;

  • Shallow Size:此对象类型使用的 Java 内存总量(以字节为单位);

  • Retained Size:为此类的所有实例而保留的内存总大小(以字节为单位);

点击一个类名称可在右侧打开 Instance View 窗口。列出的每个实例都包含以下信息:

  • Depth:从任意 GC 根到选定实例的最短跳数;

  • Native Size:原生内存中此实例的大小。 只有在使用 Android 7.0 及更高版本时,才会看到此列;

  • Shallow Size:Java 内存中此实例的大小;

  • Retained Size:此实例所支配内存的大小;

要检查应用的堆,请按以下步骤操作:

  1. 浏览列表以查找堆计数异常大且可能存在泄露的对象。为帮助查找已知类,点击 Class Name 列标题以按字母顺序排序。然后,点击一个类名称。此时右侧将出现 Instance View 窗格,显示该类的每个实例;

  2. Instance View 窗格中,点击一个实例。此时下方将出现 References 标签页,显示对该对象的每个引用;

  3. References 标签页中,如果我们发现某个引用可能在泄露内存,请右键点击它并选择 Go to Instance。这样会从堆转储中选择相应的实例,从而向我们显示它自己的实例数据。

4.2 HPROF 文件

捕获堆转储后,只有在 Memory Profiler 正在运行时,才能在该分析器中查看数据。当我们退出分析会话时,会丢失堆转储。因此,如果我们要保存堆转储以供日后查看,请将其导出到 HPROF 文件。

Sessions 窗格中每个 Heap Dump 条目的右侧都有一个 Export Heap Dump 按钮。在随即显示的 Export As 对话框中,使用 .hprof 文件扩展名保存文件。

要使用其他 HPROF 分析器(如 jhat),我们需要将 HPROF 文件从 Android 格式转换为 Java SE HPROF 格式。 我们可以使用 android_sdk/platform-tools/ 目录中提供的 hprof-conv 工具执行此操作。运行包含两个参数(即原始 HPROF 文件和转换后 HPROF 文件的写入位置)的 hprof-conv 命令。例如:

hprof-conv heap-original.hprof heap-converted.hprof

5. 小结

本节课程我们主要学习了如何分析内存活动。本节课程的重点如下:

  • 掌握如何查看内存分配和堆;
  • 掌握如何检查分析内存数据。