ART 虚拟机与内存管理高级策略(3):高级内存问题诊断

本文是「ART 虚拟机与内存管理高级策略」系列的第 3 篇,共 4 篇。在上一篇中,我们探讨了「ART 垃圾回收(GC)深度剖析」的相关内容。

三、高级内存问题诊断

理解 ART 和 GC 机制后,我们能更深入地诊断常见的内存问题。

内存泄漏(Java Heap)——根源追踪

超越 Activity/Context 泄漏

需要关注更隐蔽的泄漏源:

  • 静态集合:静态 List、Map 持有不再需要的对象引用
  • 单例持有 Context/View:单例生命周期过长,如果持有短生命周期的 Context 或 View 引用,会导致泄漏
  • 内部类/Lambda 引用:非静态内部类或 Lambda 表达式会隐式持有外部类引用。如果内部类实例(如 Handler、Thread、AsyncTask)生命周期长于外部类(如 Activity),就会泄漏外部类
  • 监听器/回调未注销:向系统服务或其他长生命周期对象注册监听器后,忘记在组件销毁时注销(unregisterReceiverremoveCallbacksremoveListener
  • 线程/线程池:线程或线程池任务持有 Activity/Fragment 引用,而线程未及时结束或被正确管理
  • 第三方库:某些库内部可能存在泄漏

诊断关键:找到泄漏对象的强引用链(GC Root Path),即从 GC Root(如静态变量、活动线程栈、JNI 引用)到泄漏对象的最短强引用路径。

内存抖动(Memory Churn)——GC 的催化剂

  • 本质:在短时间内大量创建和销毁对象
  • 危害
    • 频繁 Minor GC:导致 CPU 消耗增加,可能引发短暂卡顿
    • 对象晋升:大量短命对象可能在 Minor GC 时存活下来(因为 GC 发生时它们还在被引用),被错误地晋升到老年代,增加了老年代的压力和 Major GC 的频率
    • 堆碎片(老式 GC):虽然现代 GC 有所缓解,但极端抖动仍可能加剧碎片化
  • 常见源头
    • onDraw 中的对象创建:在 onDraw 方法内创建 Paint、Rect、Path 等对象
    • 循环中的字符串拼接:使用 + 进行字符串拼接会创建大量中间 String 和 StringBuilder 对象
    • 频繁的原始类型装箱/拆箱:在需要对象的地方使用了原始类型,或反之,导致自动装箱/拆箱
    • 不合理的数据处理:例如,逐字节读取流并反复创建小缓冲区
    • 日志库:配置不当的日志库可能在循环中创建大量字符串

堆碎片化(Heap Fragmentation)——无形杀手

  • 现象:Java 堆的总空闲内存足够,但没有连续的大块内存来满足某个大对象的分配请求,导致 OOM
  • 成因:非移动式 GC 算法(如 CMS 的清除阶段)只回收不连续的小块内存。大量不同生命周期的对象混杂也可能导致
  • 缓解:现代 ART 使用的并发复制/整理 GC(如 CC)能有效解决碎片问题。使用大对象空间(LOS)隔离大对象也能减少主堆碎片。优化内存抖动也有帮助

Bitmap 内存问题——消耗大户

  • 核心挑战:Bitmap 占用的内存通常远超其文件大小(解码后是未压缩的像素数据),且常驻内存。计算公式:宽 × 高 × 每个像素字节数。ARGB_8888 每个像素 4 字节,RGB_565 每个像素 2 字节
  • 常见陷阱
    • 加载原图:直接加载未经缩放的高分辨率图片到内存,即使显示时只需要一个小缩略图
    • 内存泄漏:Bitmap 对象被不再需要的 View 或数据结构持有
    • inBitmap 使用不当:未正确复用 Bitmap 内存(要求 API 11+,尺寸兼容,配置相同,可变)
    • 缓存策略不当:内存缓存(LruCache)过大或未正确管理大小

OOM(Out-of-Memory)Error

  • 原因多样
    • 真实泄漏:累积的内存泄漏耗尽了堆空间
    • 单次大分配:尝试分配一个超大的对象(如巨型 Bitmap、超长数组)超过了堆剩余空间或连续空间限制
    • 碎片化:如前述,总空间够但连续空间不足
    • 并发分配压力:多个线程同时请求大量内存
    • Native 内存耗尽:Java 堆还有空间,但进程总内存(包括 Native)触及系统上限
    • 虚拟机限制:早期 Android 版本或低端设备上,单个应用的堆大小限制较低
  • 诊断:OOM 时的 Heap Dump 是关键。分析是哪个线程、尝试分配什么类型的对象、多大时失败的。结合 dumpsys meminfo 查看当时的总体内存分布。

四、内存分析利器

精通工具是解决复杂内存问题的关键。

Android Studio Profiler(Memory)

  • 实时监控:观察 Java Heap、Native Heap、Code、Graphics 等内存变化趋势,快速发现异常增长
  • Allocation Tracking:启动/停止录制对象分配。分析特定操作期间(如进入某个页面、执行某个功能)创建了哪些对象、数量、大小和调用栈。重点用于定位内存抖动来源。 注意其性能开销
  • Heap Dump:手动触发或在 OOM 时自动捕获。可以直接在 Profiler 中进行初步分析(查看类实例、引用关系),但复杂分析建议导出 HPROF 并在 MAT 中打开
  • GC 事件:时间线上会标记 GC 事件,可以观察 GC 发生频率和对应用行为的影响

MAT(Memory Analyzer Tool)——堆转储分析神器

核心功能

  • Dominator Tree(支配树)最重要的视图! 显示对象间的支配关系。对象 A 支配对象 B,意味着所有指向 B 的强引用路径都必须经过 A。支配树的根节点是 GC Roots。通过查看占用 Retained Size(该对象及其支配的所有对象总大小)最大的节点,可以快速找到内存消耗的主要源头。逐层展开支配节点,分析其子节点构成,找到不合理持有大内存的对象
  • Histogram(直方图):按类名列出所有实例的数量、Shallow Heap(对象自身大小)和 Retained Heap。用于快速发现:
    • 实例数量异常多的类(可能是泄漏或缓存问题)
    • Shallow Heap 异常大的类(如超大 byte[]、String)
    • Retained Heap 异常大的类(支配了大量内存)
  • Leak Suspects Report(泄漏嫌疑报告):MAT 自动分析并给出可能的内存泄漏点(通常是 Activity、Fragment、Bitmap 等),并展示到 GC Root 的引用链。是分析泄漏的起点

OQL(Object Query Language)

类 SQL 语言,用于对 Heap Dump 执行复杂查询。极度强大!

示例

SELECT * FROM instanceof android.app.Activity

(查找所有 Activity 实例)

SELECT * FROM android.graphics.Bitmap bmp WHERE bmp.mWidth > 1920

(查找宽度大于 1920 的 Bitmap)

SELECT toString(o.key) FROM java.util.HashMap$Node o WHERE o.key.@clazz.getName() = "com.example.MyKeyClass"

(查找特定 Key 类型的 HashMap 条目)

SELECT * FROM MATCHER dominators(OBJECT_ADDRESS)

(查找支配指定对象的对象)

  • Path to GC Roots:右键点击对象,选择「Path to GC Roots」→「with all references」,查找阻止对象被回收的引用链
  • Merge Shortest Paths to GC Roots:查看一个对象集合的所有到 GC Root 的最短强引用路径
  • Compare Heap Dumps(对比堆转储):加载两个不同时间点的 Heap Dump,MAT 可以分析出两者之间的差异(哪些对象增加了,哪些减少了),用于定位特定操作导致的内存增长

Perfetto(UI 与 Memory 联合分析)

内存相关 Track

  • mem.java_heap:Java 堆分配大小
  • mem.native_heap:Native 堆分配大小
  • mem.graphics:图形内存(主要是 GL 纹理、Buffer 等)
  • mem.total_pss:进程的总 PSS 内存(Proportional Set Size,按比例共享内存)
  • mem.locked:锁定内存(如 mlock()
  • mem.rss:进程的总 Resident Set Size

内存事件

  • Memory Counters:上述 Track 记录的都是周期性采样的 Counter 值
  • Heap Graph / Heap Profile(需配置数据源):可以记录 Java/Native 堆的详细分配信息(类似 Profiler,但集成在 Perfetto Trace 中),开销较大
  • Java Heap GC Events:记录 GC 的开始、结束、暂停时间

分析价值

Perfetto 的最大优势在于关联分析。可以将内存使用量的突增、GC 暂停事件与同一时间轴上的 UI Jank 事件(Actual frame timeline vs Expected frame timeline)、CPU 调度(CPU Scheduling)、Binder 事务(Binder transactions)等关联起来,判断内存问题是否是导致性能问题的直接原因。例如,观察到一次 Jank 是否紧随一次长时间的 GC 暂停。

命令行工具

adb shell dumpsys meminfo <package_name|pid>

解读:必须理解输出中各个字段的含义:

  • PSS Total:按比例计算的共享内存 + 私有内存,衡量进程实际消耗物理内存的较好指标
  • Private Dirty:进程私有的、已被修改的 RAM。是进程独占且系统无法换出的主要部分
  • Private Clean:进程私有的、未被修改的 RAM(如从文件映射的代码、资源)。系统在内存不足时可以换出这部分
  • Swap PSS:进程使用的交换空间(ZRAM)大小(如果启用)
  • Heap Size / Heap Alloc / Heap Free:Java 堆的总大小、已分配大小、空闲大小
  • Native Heap:Pss、Private Dirty、Private Clean 分别统计 Native 部分的内存
  • Stack:Java 和 Native 线程栈
  • Graphics:图形相关内存(驱动、纹理缓存等)
  • Code:应用代码(DEX、OAT、.so)占用的内存
  • Ashmem、GL mtrack、Unknown:其他共享内存或无法精确归类的内存
  • --unreachable(Android 11+):显示当前 Java 堆中不可达(但 GC 尚未回收)的对象内存大小,有助于了解潜在的 GC 压力

adb shell am dumpheap <pid> /sdcard/heap.hprof:手动触发指定进程的 Heap Dump 并保存到设备。

adb bugreport:生成包含大量系统状态(包括 meminfo、procrank 等)的 Bugreport 压缩包,用于离线分析。



下一篇我们将探讨「Native 内存探秘:冰山下的部分」,敬请关注本系列。

「ART 虚拟机与内存管理高级策略」系列目录

  1. 引言:性能与稳定的基石
  2. ART 垃圾回收(GC)深度剖析
  3. 高级内存问题诊断(本文)
  4. Native 内存探秘:冰山下的部分