Android perfetto - 如何分析memory

90 阅读12分钟

1、简述

在开发 Android 应用程序时,内存管理是一个关键的性能因素。过多的内存使用可能导致应用崩溃、性能下降,甚至影响整个系统的响应速度。理解 Android 内存使用情况以及如何优化内存占用至关重要。本文将介绍如何使用perfetto工具,并结合网上关于Linux 的内存管理原理,介绍如何来调试和优化 Android 内存使用。本文讲的四个重点内容:

  • 使用 dumpsys meminfo 获取内存使用概览;
  • 理解 Linux 内存管理的基本概念;
  • 使用 Perfetto 调查一段时间内的内存使用情况;
  • 分析native heap profiles 和 Java heap dumps,以识别内存泄漏。

2、dumpsys meminfo

dumpsys meminfo 是一个非常有用的工具,可以提供进程的内存使用概况,帮助我们了解不同类型的内存(如本地堆、Dalvik 堆等)的使用情况。

示例命令:

$ adb shell dumpsys meminfo com.android.systemui

输出示例:

Applications Memory Usage (in Kilobytes):
Uptime: 19410770 Realtime: 19410770

** MEMINFO in pid 4210 [com.android.systemui] **
                   Pss  Private  Private  SwapPss      Rss     Heap     Heap     Heap
                 Total    Dirty    Clean    Dirty    Total     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------   ------
  Native Heap    23816    20592     3196    75022    24780   124864   107218    12451
  Dalvik Heap    48842    32200    16564    92616    51024   143410    45138    98272
 Dalvik Other    24675     8244    13688     4331    38016 

通过查看 Dalvik Heap(即 Java 堆)和Native Heap的“Private Dirty”列,我们可以看到 SystemUI 在 Java 堆上的内存使用为 32MB,在本地堆上的内存使用为 20MB。要理解 dumpsys meminfo输出中的内存信息,我们需要了解一些与 Linux 内存管理相关的核心概念,包括CleanDirtyRSSPSSSwap。这些概念帮助我们理解进程如何使用内存,并提供了优化内存使用的依据。

3、Linux 内存管理的基本概念

从 Linux 内核的角度来看,内存是由一块块相同大小的块(称为“页面”)组成的。每个页面通常是 4 KiB 大小。内存中的这些页面并不是随意分布的,它们被组织成虚拟上连续的区域,称为 虚拟内存区域(VMA,Virtual Memory Area)。当应用程序需要新的内存区域时,会通过mmap()系统调用来请求这些内存页面。实际分配内存的操作通常是由分配器(如native进程中的malloc()或 C++ 的new,或 Android 中的Runtime)间接完成的。

内存的页面可以是两种类型之一:

  • 文件支持的 VMA(File-backed VMA):表示内存中的文件视图。文件支持的内存通常用于动态加载库、执行文件或访问 APK 中的资源。例如,动态链接器(ld)会使用这种方式加载新进程或动态库。
  • 匿名 VMA(Anonymous VMA):这种内存区域不与任何文件关联,通常用于分配动态内存,如malloc()new 请求的内存。

物理内存的分配是按页面粒度进行的,这意味着当进程请求分配内存时,内存并不会立即分配到物理内存。只有当进程尝试读取或写入这些内存页面时,内核才会为其分配物理内存。假设分配了 32 MiB 的虚拟内存,但只有一个字节被访问,那么物理内存的使用量只会增加 4 KiB(即一个页面)。此时,虽然虚拟内存的使用增加了 32 MiB,但进程的常驻物理内存(RSS)只增加了 4 KiB。

3.1、RSS (Resident Set Size) — 常驻集大小

RSS 表示进程占用的物理内存的总量,即进程实际驻留在内存中的页面的总大小。它是衡量进程物理内存使用情况的一个重要指标。当进程请求内存时,内核将虚拟内存页面映射到物理内存,这部分物理内存就是 RSS。

3.2、Clean 和 Dirty 页面

Linux 内存中的页面可以有两种基本状态:CleanDirty。这两者的区别在于页面的内容是否已被修改。

  • Clean 页面:对于文件支持的内存页面,Clean 表示该页面的内容与磁盘上的文件内容一致。内核可以在内存紧张时回收这类页面,因为它知道可以通过从磁盘读取文件来恢复这些页面的内容。
  • Dirty 页面Dirty 页面表示该页面的内容与磁盘上的文件内容不一致,或者该页面没有磁盘支持(即匿名内存)。通常,匿名内存的页面被标记为脏页面。脏页面不能被简单地驱逐,因为这会导致数据丢失。但如果系统存在交换空间(Swap Space)或 ZRAM(内存压缩交换空间),脏页面可以被写入到交换空间中。

3.3、Swapped 页面

当物理内存不足时,脏页面可以被交换到磁盘上的交换空间(Swap Space)或压缩到 ZRAM 中。这个过程叫做页面交换(Swapping) 。交换后的页面会在被再次访问时重新加载到内存中。换句话说,交换是一种将不活跃的页面暂时移到磁盘的方式,以释放内存给其他更重要的进程使用。

对于应用程序来说,过多的交换内存可能会影响性能,因为磁盘 I/O 的速度远低于内存访问速度。如果发现进程的交换内存较高,说明该进程可能占用了过多的内存,导致系统需要频繁进行交换操作。

3.4、PSS (Proportional Set Size) — 比例集大小

PSS 是一个更精确的度量标准,用于衡量进程的内存使用。它考虑了多个进程共享相同内存的情况。当多个进程共享同一页面时,PSS 会将该页面的占用比例分摊到每个进程。例如,如果一个 4 KiB 的页面被 4 个进程共享,那么每个进程的 PSS 会增加 1 KiB。

PSS 值较高的进程可能在内存共享方面有较高的占用,需要特别关注。通过分析 PSS 值,可以帮助开发者了解哪些进程正在共享大量内存,并优化内存使用。

3.5、Not Present 页面

Not Present 状态表示页面从未被访问过,或者该页面是清洁的并且后来被驱逐。在这种情况下,内存页面不会被加载到物理内存中,因此它不占用物理内存。只有在页面被访问时,内核才会为其分配物理内存。

3.6、小结

理解了上述内存管理概念后,我们可以使用这些信息来优化应用程序的内存使用:

  • 减少脏内存的占用:过多的脏内存不能被回收,因此它会增加系统内存的压力。应该尽量减少不必要的动态内存分配,特别是匿名内存的使用,确保及时释放内存。
  • 监控 PSS 值:PSS 值较高的进程可能在共享内存方面占用了较多资源。如果某个进程的 PSS 值过高,可能表明它在共享内存方面存在问题,或者在内存泄漏。
  • 避免过多的交换操作:当进程的内存被交换到磁盘时,会导致性能下降。通过优化应用的内存占用,减少不必要的内存使用,避免系统频繁进行交换操作。
  • 及时释放内存:动态分配的内存必须在使用完毕后及时释放。避免内存泄漏,确保进程不会占用过多的脏内存。

4、Perfetto跟踪内存变化

dumpsys meminfo可以帮助我们获取当前的内存使用快照,但它只展示了静态的内存状态。即使是非常短暂的内存峰值也可能会导致系统内存不足,进而触发低内存杀手(LMK) 来回收内存。因此,单次的内存快照可能不足以全面反映内存使用情况。

为了解决这个问题,我们可以借助以下两种工具来深入分析内存使用的变化:

  1. RSS 高水位标记(RSS High Watermark)
  2. 内存 tracepoints(内存追踪点)

4.1、RSS 高水位标记(RSS High Watermark)

RSS 高水位标记是用于跟踪进程内存使用的一个有用指标。它显示了进程自启动以来所经历的最大常驻集大小(RSS) 。在进程生命周期中,内核会不断更新该值,以确保它始终代表进程实际占用的物理内存的峰值。

  • VmHWM:这个值记录了进程自启动以来所见过的最大 RSS 使用量。这个值是由内核维护和更新的,反映了进程内存使用的高水位。

通过查看/proc/[pid]/status文件中的VmHWM 字段,我们可以获得进程的最大内存使用信息。如果该值较高,说明进程曾经在某些时刻占用了较大的内存,这可能是系统内存压力的一个预兆,尤其是在短时间内发生过大的内存波动时。

示例命令

...
VmLck:         0 kB
VmPin:         0 kB
VmHWM:    684584 kB
VmRSS:    516196 kB
RssAnon:    222448 kB
RssFile:    271604 kB
...

该命令可以帮助我们提取当前进程的VmHWM 值,从而了解其最大内存使用情况。为什么 RSS 高水位标记重要?

  • 内存峰值: 如果应用经历了短暂的内存高峰,而这些峰值未被捕捉到,系统可能会忽略内存压力。
  • 低内存风险: 如果进程的内存使用超过了设备的物理内存容量,它会触发低内存杀手(LMK),因此监控内存峰值是很重要的,尤其是在内存压力大的情况下。

4.2、Perfetto内存追踪(Memory tracepoints)

我们可以使用 Perfetto 获取来自内核的内存管理事件信息。

data_sources: {
    config {
        name: "linux.ftrace"
        ftrace_config {
            ftrace_events: "mm_event/mm_event_record"
            ftrace_events: "kmem/rss_stat"
            ftrace_events: "kmem/ion_heap_grow"
            ftrace_events: "kmem/ion_heap_shrink"
        }
    }
}

image.png

这个展示是相机应用打开之后内存的增长情况。我们可以通过 内存追踪点(tracepoints) 来监控内存使用的动态变化。Tracepoints 是内核提供的用于记录内存事件的机制,允许开发者追踪进程在运行过程中内存的变化。这些追踪点可以帮助我们识别内存使用的短期波动和内存峰值。

5、Perfetto分析Native Heap和Java Heap

Android Userdebug版本可以对所有应用和大多数系统服务进行Heap Profile。而在User版本中,只能对具有debuggableprofileable 清单标志的应用进行性能分析。更多抓取Native Heap和Java Profiling信息,请参考Android perfetto - 记录分析memory文档。

5.1、Native Heap内存分析报告中的标签说明

image.png

  1. 未释放的 malloc 大小(Unreleased malloc size)

    表示在创建当前转储时,某个调用栈上已分配但尚未释放的内存大小。也就是说,这是当前堆栈中分配的内存,且这些内存尚未通过free 或其他释放机制被释放。可以帮助我们识别内存泄漏,或者那些没有及时释放的内存。

  2. 总 malloc 大小(Total malloc size)

    表示在当前调用栈下分配的所有内存总大小,包括那些已释放的内存。即使这些内存已被释放,它们仍会被计算在内。给出某个调用栈在其生命周期内使用的总内存量,这有助于评估整个调用栈的内存使用情况。

  3. 未释放的 malloc 次数(Unreleased malloc count)

    表示在当前调用栈下,进行内存分配但没有相应释放操作的次数。即当前调用栈中进行了多少次内存分配,但这些内存分配未被释放。可以帮助识别内存泄漏的频率,或者哪些函数调用会导致内存分配但未释放。

  4. 总 malloc 次数(Total malloc count)

    表示在当前调用栈下进行的所有内存分配的次数,包括那些有对应释放操作的分配。即无论内存是否释放,所有的分配都会计入此统计。用于评估在某个调用栈中的内存分配活动的总体频率,帮助了解内存分配的总体情况。

5.2、Java Heap内存分析报告中的标签说明

image.png

  1. Object Size

    显示通过该路径到垃圾回收根的内存保留大小,以字节为单位。帮助分析通过该路径的对象所占用的内存大小。

  2. Object Count

    显示通过该路径到垃圾回收根的对象数量。帮助分析通过该路径的对象总数。

  3. Dominated Size:

    表示在一个支配树节点中,哪些字节是被节点中的对象唯一保留的。分析哪些对象是被支配树节点中的对象独占的内存占用。

  4. Dominated Objects:

    表示在一个支配树节点中,哪些对象是被节点中的对象唯一保留的。分析哪些对象是支配树节点中的对象独占的。

5.3、最佳实践建议

  1. 合理设置采样率:过小的采样间隔会影响性能,过大则可能遗漏重要信息
  2. 针对性监控:不要同时监控过多进程,避免数据过大
  3. 多个数据源结合分析:与CPU profiling、ftrace等数据联合分析

6、参考