使用内存分析器检查应用的内存使用情况

内存分析器是 Android 分析器 中的一个组件,可帮助您识别可能导致卡顿、冻结甚至应用崩溃的内存泄漏和内存抖动。它显示应用内存使用的实时图表,并允许您捕获堆转储、强制进行垃圾回收以及跟踪内存分配。

要打开内存分析器,请按照以下步骤操作

  1. 点击**查看 > 工具窗口 > 分析器**(您也可以点击工具栏中的**分析** )。
  2. 从 Android 分析器工具栏中选择要分析的设备和应用进程。如果您已通过 USB 连接设备但未看到其列出,请确保您已启用 USB 调试
  3. 点击**内存**时间线上的任意位置即可打开内存分析器。

或者,您可以使用dumpsys从命令行检查应用内存,也可以查看 logcat 中的 GC 事件

为什么要分析应用内存

Android 提供了托管内存环境——当它确定您的应用不再使用某些对象时,垃圾回收器会将未使用的内存释放回堆。Android 如何查找未使用的内存一直在不断改进,但在所有 Android 版本上,系统最终都必须短暂暂停您的代码。大多数情况下,暂停是无法察觉的。但是,如果您的应用分配内存的速度快于系统收集内存的速度,则您的应用可能会在收集器释放足够的内存以满足您的分配请求时延迟。此延迟可能会导致您的应用跳过帧并导致明显的缓慢。

即使您的应用没有表现出缓慢,如果它泄漏内存,它也可能在后台保留该内存。此行为可能会通过强制执行不必要的垃圾回收事件来降低系统其余部分的内存性能。最终,系统被迫终止您的应用进程以回收内存。然后,当用户返回您的应用时,它必须完全重新启动。

为帮助防止这些问题,您应该使用内存分析器执行以下操作

  • 查找时间线中可能导致性能问题的 undesirable 内存分配模式。
  • 转储 Java 堆以查看哪些对象在任何给定时间都在使用内存。在较长时间内进行多次堆转储可以帮助识别内存泄漏。
  • 在正常和极端用户交互期间记录内存分配,以准确识别您的代码是在短时间内分配了太多对象,还是分配了最终泄漏的对象。

有关可减少应用内存使用的编程实践的信息,请阅读管理应用的内存

内存分析器概述

首次打开内存分析器时,您将看到应用内存使用的详细时间线,以及用于强制垃圾回收、捕获堆转储和记录内存分配的工具。

图 1. 内存分析器

如图 1 所示,内存分析器的默认视图包括以下内容:

  1. 一个用于强制进行垃圾回收事件的按钮。
  2. 一个用于捕获堆转储的按钮。

    注意:只有在连接到运行 Android 7.1(API 级别 25)或更低版本的设备时,用于记录内存分配的按钮才会出现在堆转储按钮的右侧。

  3. 一个下拉菜单,用于指定分析器捕获内存分配的频率。选择合适的选项可以帮助您在分析时提高应用性能
  4. 用于放大/缩小时间线的按钮。
  5. 一个用于跳转到实时内存数据的按钮。
  6. 事件时间线,显示活动状态、用户输入事件和屏幕旋转事件。
  7. 内存使用时间线,包括以下内容:
    • 每个内存类别使用的内存量的堆叠图,如左侧的 y 轴和顶部的颜色键所示。
    • 虚线表示已分配对象的数目,如右侧的 y 轴所示。
    • 每个垃圾回收事件的图标。

但是,如果您使用的是运行 Android 7.1 或更低版本的设备,则默认情况下并非所有分析数据都可见。如果您看到一条消息显示“对于所选进程,高级分析不可用”,则需要启用高级分析才能查看以下内容:

  • 事件时间线
  • 已分配对象的数目
  • 垃圾回收事件

在 Android 8.0 及更高版本上,始终为可调试应用启用高级分析。

内存计数方式

您在内存分析器顶部看到的数字(图 2)基于您的应用已提交的所有私有内存页,根据 Android 系统。此计数不包括与系统或其他应用共享的页面。

图 2. 内存计数图例位于内存分析器顶部

内存计数中的类别如下所示

  • Java:来自 Java 或 Kotlin 代码分配的对象的内存。
  • Native:来自 C 或 C++ 代码分配的对象的内存。

    即使您的应用未使用 C++,您也可能会看到此处使用了一些本地内存,因为 Android 框架使用本地内存来代表您处理各种任务,例如处理图像资源和其他图形 — 即使您编写的代码是用 Java 或 Kotlin 编写的。

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

  • Stack:您的应用中本地和 Java 堆栈使用的内存。这通常与您的应用正在运行多少线程有关。

  • Code:您的应用用于代码和资源的内存,例如 dex 字节码、优化或编译的 dex 代码、.so 库和字体。

  • Others:您的应用使用的系统不确定如何分类的内存。

  • Allocated:您的应用分配的 Java/Kotlin 对象的数量。这并不计算在 C 或 C++ 中分配的对象。

    连接到运行 Android 7.1 及更低版本的设备时,此分配计数仅从内存分析器连接到您的运行应用时开始。因此,在您开始分析之前分配的任何对象都不会被计算在内。但是,Android 8.0 及更高版本包含一个设备上的分析工具,可以跟踪所有分配,因此此数字始终代表 Android 8.0 及更高版本上您的应用中所有未完成的 Java 对象的总数。

与之前的 Android Monitor 工具中的内存计数相比,新的内存分析器以不同的方式记录您的内存,因此您的内存使用量似乎现在更高了。内存分析器监视一些额外类别,这些类别会增加总数,但如果您只关心 Java 堆内存,则“Java”数字应该与之前工具中的值类似。虽然 Java 数字可能与您在 Android Monitor 中看到的数字不完全匹配,但新数字会计算自从它从 Zygote 分支以来分配给您的应用 Java 堆的所有物理内存页。因此,这提供了您的应用实际使用多少物理内存的准确表示。

查看内存分配

内存分配向您展示了内存中每个 Java 对象和 JNI 引用是如何分配的。具体来说,内存分析器可以向您显示以下有关对象分配的信息

  • 分配了哪些类型的对象以及它们使用了多少空间。
  • 每个分配的堆栈跟踪,包括在哪个线程中。
  • 对象何时被释放(仅当使用运行 Android 8.0 或更高版本的设备时)。

要记录 Java 和 Kotlin 分配,请选择记录 Java/Kotlin 分配,然后选择记录。如果设备运行的是 Android 8 或更高版本,则内存分析器 UI 会转换到显示正在进行的记录的单独屏幕。您可以与记录上方的迷你时间线交互(例如,更改选择范围)。要完成记录,请选择停止

Visualization of Java allocations in Memory Profiler

在 Android 7.1 及更低版本上,内存分析器使用旧版分配记录,该记录会在您点击停止之前显示时间线上的记录。

选择时间线的一个区域后(或完成使用运行 Android 7.1 或更低版本的设备的记录会话后),分配的对象列表将显示,按类名分组并按其堆计数排序。

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

  1. 浏览列表以查找堆计数异常大的并且可能泄漏的对象。为了帮助查找已知的类,单击类名列标题按字母顺序排序。然后单击类名。实例视图窗格将显示在右侧,显示该类的每个实例,如图 3 所示。
    • 或者,您可以通过单击筛选 或按 Control+F(在 Mac 上按 Command+F)并在搜索字段中输入类名或包名来快速找到对象。如果您从下拉菜单中选择按调用堆栈排列,也可以按方法名称搜索。如果您想使用正则表达式,请选中正则表达式旁边的框。如果您的搜索查询区分大小写,请选中区分大小写旁边的框。
  2. 实例视图窗格中,单击一个实例。调用堆栈选项卡将显示在下方,显示该实例在何处以及在哪个线程中分配。
  3. 调用堆栈选项卡中,右键单击任意一行并选择跳转到源以在编辑器中打开该代码。

图 3. 分配对象的详细信息显示在右侧的实例视图

您可以使用分配对象列表上方的两个菜单来选择要检查的堆以及如何组织数据。

从左侧菜单中,选择要检查的堆

  • default heap:系统未指定堆时。
  • image heap:系统引导映像,包含在引导时预加载的类。此处分配保证永远不会移动或消失。
  • zygote heap:Android 系统中应用进程从中派生的写时复制堆。
  • app heap:您的应用分配内存的主要堆。
  • JNI heap:显示 Java 本机接口 (JNI) 引用在何处分配和释放的堆。

从右侧菜单中,选择如何排列分配

  • 按类排列:根据类名对所有分配进行分组。这是默认设置。
  • 按包排列:根据包名对所有分配进行分组。
  • 按调用堆栈排列:将所有分配分组到其相应的调用堆栈中。

在分析期间提高应用性能

为了在分析期间提高应用性能,内存分析器默认情况下会定期采样内存分配。在运行 API 级别 26 或更高版本的设备上进行测试时,您可以使用分配跟踪下拉菜单更改此行为。可用的选项如下所示

  • Full:捕获内存中所有对象分配。这是 Android Studio 3.2 和更早版本中的默认行为。如果您有一个分配大量对象的应用,您可能会在分析期间观察到应用出现明显的减速。
  • Sampled:定期对内存中的对象分配进行采样。这是默认选项,对分析期间的应用性能的影响较小。在短时间内分配大量对象的应用仍然可能会出现明显的减速。
  • Off:停止跟踪您的应用的内存分配。

查看全局 JNI 引用

Java 本机接口 (JNI) 是一个允许 Java 代码和本机代码相互调用的框架。

JNI 引用由本机代码手动管理,因此本机代码使用的 Java 对象可能会保持活动状态过长。如果在未首先显式删除的情况下丢弃 JNI 引用,则 Java 堆上的某些对象可能变得不可访问。此外,还可能耗尽全局 JNI 引用限制。

要解决此类问题,请使用内存分析器中的JNI 堆视图来浏览所有全局 JNI 引用并按 Java 类型和本机调用堆栈对其进行筛选。通过这些信息,您可以找到创建和删除全局 JNI 引用的时间和位置。

在您的应用运行时,选择您要检查的时间线的一部分,然后从类列表上方的下拉菜单中选择JNI 堆。然后,您可以像平时一样检查堆中的对象,并双击分配调用堆栈选项卡中的对象以查看 JNI 引用在代码中的分配和释放位置,如图 4 所示。

图 4. 查看全局 JNI 引用

要检查应用 JNI 代码的内存分配,您必须将应用部署到运行 Android 8.0 或更高版本的设备。

有关 JNI 的更多信息,请参阅JNI 提示

本地内存分析器

Android Studio 内存分析器包含一个本地内存分析器,用于部署到运行 Android 10 及更高版本的物理和虚拟设备的应用。

本地内存分析器在特定时间段内跟踪本机代码中对象的分配/释放,并提供以下信息

  • Allocations:在选定时间段内通过malloc()new运算符分配的对象计数。
  • Deallocations:在选定时间段内通过free()delete运算符释放的对象计数。
  • Allocations Size:选定时间段内所有分配的总大小(以字节为单位)。
  • Deallocations Size:选定时间段内所有已释放内存的总大小(以字节为单位)。
  • Total Count:Allocations列中的值减去Deallocations列中的值。
  • 剩余大小:已分配大小列中的值减去已释放大小列中的值。

Native Memory Profiler

要在运行 Android 10 及更高版本的设备上记录原生分配,请选择记录原生分配,然后选择记录。录制将持续进行,直到您点击停止 ,之后内存分析器 UI 将切换到另一个屏幕,显示原生录制内容。

Record native allocations button

在 Android 9 及更低版本中,记录原生分配选项不可用。

默认情况下,原生内存分析器使用 32 字节的采样大小:每次分配 32 字节的内存时,都会拍摄内存快照。较小的采样大小会导致更频繁的快照,从而产生更准确的内存使用情况数据。较大的采样大小会产生不太准确的数据,但它会消耗系统较少的资源,并在录制过程中提高性能。

要更改原生内存分析器的采样大小:

  1. 选择运行 > 编辑配置
  2. 在左侧窗格中选择您的应用模块。
  3. 点击分析选项卡,并在标记为原生内存采样间隔(字节)的字段中输入采样大小。
  4. 重新构建并运行您的应用。

捕获堆转储

堆转储显示在捕获堆转储时应用中哪些对象正在使用内存。尤其是在长时间的用户会话之后,堆转储可以通过显示您认为不再需要的内存中对象来帮助识别内存泄漏。

捕获堆转储后,您可以查看以下内容:

  • 您的应用已分配哪些类型的对象,以及每种类型的对象数量。
  • 每个对象正在使用多少内存。
  • 在您的代码中保存对每个对象的引用位置。
  • 分配对象的调用堆栈。(只有在录制分配期间捕获堆转储时,Android 7.1 及更低版本才可以使用堆转储提供调用堆栈。)

要捕获堆转储,请点击捕获堆转储,然后选择记录。在转储堆的过程中,Java 内存量可能会暂时增加。这是正常的,因为堆转储发生在与您的应用相同的进程中,并且需要一些内存来收集数据。

分析器完成捕获堆转储后,内存分析器 UI 将切换到另一个屏幕,显示堆转储。

图 5. 查看堆转储。

如果您需要更精确地确定转储创建的时间,可以通过调用dumpHprofData()在应用代码的关键点创建堆转储。

在类列表中,您可以看到以下信息:

  • 分配:堆中分配的数量。
  • 原生大小:此对象类型使用的原生内存总量(以字节为单位)。此列仅在 Android 7.0 及更高版本中可见。

    您将在这里看到一些在 Java 中分配的对象的内存,因为 Android 使用原生内存来处理一些框架类,例如Bitmap

  • 浅层大小:此对象类型使用的 Java 内存总量(以字节为单位)。

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

您可以使用分配对象列表上方的两个菜单来选择要检查的堆转储以及如何组织数据。

从左侧菜单中,选择要检查的堆

  • default heap:系统未指定堆时。
  • app heap:您的应用分配内存的主要堆。
  • image heap:系统引导映像,包含在引导时预加载的类。此处分配保证永远不会移动或消失。
  • zygote heap:Android 系统中应用进程从中派生的写时复制堆。

从右侧菜单中,选择如何排列分配

  • 按类排列:根据类名对所有分配进行分组。这是默认设置。
  • 按包排列:根据包名对所有分配进行分组。
  • 按调用堆栈排列:将所有分配分组到其相应的调用堆栈中。此选项仅在录制分配期间捕获堆转储时有效。即便如此,堆中也可能存在在您开始录制之前分配的对象,因此这些分配首先按类名列出。

默认情况下,列表按保留大小列排序。要按其他列的值排序,请点击该列的标题。

点击类名,可在右侧打开实例视图窗口(如图 6 所示)。每个列出的实例都包括以下内容:

  • 深度:从任何 GC 根到所选实例的最短跳转数。
  • 原生大小:此实例在原生内存中的大小。此列仅在 Android 7.0 及更高版本中可见。
  • 浅层大小:此实例在 Java 内存中的大小。
  • 保留大小:此实例支配的内存大小(根据支配树)。

图 6. 捕获堆转储所需的时间在时间线上显示。

要检查堆,请按照以下步骤操作:

  1. 浏览列表以查找堆计数异常大的可能泄漏的对象。为了帮助查找已知类,请点击类名列标题按字母顺序排序。然后点击类名。实例视图窗格将出现在右侧,显示该类的每个实例,如图 6 所示。
    • 或者,您可以通过单击筛选 或按 Control+F(在 Mac 上按 Command+F)并在搜索字段中输入类名或包名来快速找到对象。如果您从下拉菜单中选择按调用堆栈排列,也可以按方法名称搜索。如果您想使用正则表达式,请选中正则表达式旁边的框。如果您的搜索查询区分大小写,请选中区分大小写旁边的框。
  2. 实例视图窗格中,点击一个实例。引用选项卡将出现在下方,显示对该对象的每个引用。

    或者,点击实例名称旁边的箭头以查看其所有字段,然后点击字段名称以查看其所有引用。如果要查看字段的实例详细信息,请右键点击该字段并选择转到实例

  3. 引用选项卡中,如果您确定可能存在内存泄漏的引用,请右键点击它并选择转到实例。这将从堆转储中选择相应的实例,显示其自身的实例数据。

在堆转储中,查找由以下任何原因引起的内存泄漏:

  • ActivityContextViewDrawable以及可能保存对ActivityContext容器的引用的其他对象的长期引用。
  • 非静态内部类,例如Runnable,它可以保存Activity实例。
  • 比必要时间更长时间保存对象的缓存。

将堆转储保存为 HPROF 文件

捕获堆转储后,只有在分析器运行时才能在内存分析器中查看数据。退出分析会话时,堆转储将会丢失。因此,如果您想保存它以备日后查看,请将堆转储导出到 HPROF 文件。在 Android Studio 3.1 及更低版本中,将捕获导出到文件 按钮位于时间线下方工具栏的左侧;在 Android Studio 3.2 及更高版本中,会话窗格中每个堆转储条目右侧都有一个导出堆转储按钮。在出现的另存为对话框中,使用.hprof 文件名扩展名保存文件。

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

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

导入堆转储文件

要导入 HPROF(.hprof)文件,请点击会话窗格中的启动新的分析会话 ,选择从文件加载,然后从文件浏览器中选择文件。

您还可以通过将 HPROF 文件从文件浏览器拖动到编辑器窗口来导入它。

内存分析器中的内存泄漏检测

在内存分析器中分析堆转储时,您可以过滤 Android Studio 认为可能指示应用中ActivityFragment实例内存泄漏的分析数据。

过滤器显示的数据类型包括:

  • 已被销毁但仍在引用的Activity实例。
  • 没有有效FragmentManager但仍在引用的Fragment实例。

在某些情况下,例如以下情况,过滤器可能会产生误报:

  • 已创建Fragment,但尚未使用。
  • Fragment正在缓存中,但不是作为FragmentTransaction的一部分。

要使用此功能,请首先捕获堆转储导入堆转储文件到 Android Studio。要显示可能存在内存泄漏的片段和活动,请在内存分析器的堆转储窗格中选择Activity/Fragment 泄漏复选框,如图 7 所示。

Profiler: Memory Leak Detection

图 7. 过滤堆转储以查找内存泄漏。

分析内存的技巧

在使用内存分析器时,您应该对应用代码进行压力测试,并尝试强制内存泄漏。在检查堆之前让应用运行一段时间是引发应用中内存泄漏的一种方法。泄漏可能会逐渐到达堆中分配的顶部。但是,泄漏越小,您需要运行应用的时间就越长才能看到它。

您还可以通过以下方式之一触发内存泄漏:

  • 在不同的活动状态下,多次将设备从纵向旋转到横向,然后再旋转回纵向。旋转设备常常会导致应用程序泄漏ActivityContextView对象,因为系统会重新创建Activity,如果您的应用程序在其他地方持有对这些对象的引用,系统就无法对其进行垃圾回收。
  • 在不同的活动状态下,在您的应用程序和其他应用程序之间切换(导航到主屏幕,然后返回您的应用程序)。

提示:您也可以使用monkeyrunner测试框架执行上述步骤。