Heap Dump:什么是堆转储、堆分析和解决内存泄漏

作者: IT Sectr 发布日期: 2026-05-07 阅读时间: 10 分钟

Heap Dump(堆转储)——应用程序动态内存的快照,包含所有存活对象的完整信息:它们的类、大小、相互引用以及从根节点(GC roots)的可达性。Heap Dump 是分析内存泄漏和优化资源消耗的主要工具。根据 Android Developers,堆转储分析可以检测多达 95% 的内存泄漏,包括循环引用、被遗忘的 listener 以及未释放的静态引用。

要点

  • Heap Dump——应用程序整个动态内存的快照,包含每个对象及其之间引用的信息。
  • Android Studio Memory Profiler 允许实时捕获 Java 和 Kotlin 应用程序的堆转储。
  • Xcode Instruments 提供 Allocations 工具,用于在 iOS/macOS 上创建和分析堆转储。
  • Shallow 和 retained size 是关键指标:shallow 是对象本身的大小,retained 是对象的大小加上它持有的所有对象。
  • 分析堆转储包括查找 dominator tree、最大的 retained objects 以及到 GC roots 的最短路径。

什么是堆转储以及为什么需要它

堆转储是虚拟机堆的完整转储——所有动态创建的对象所在的内存区域。在 Java 和 Kotlin 中,这是 Android 上的 Dalvik/ART;在 Swift 和 Objective-C 中,这是 iOS 上由 ARC 管理的堆。堆转储记录每个对象、其类、大小、字段、对其他对象的引用以及从 GC roots(栈变量、静态字段、JNI 引用)的可达性标志。

堆转储的主要目的是检测内存泄漏。当应用程序继续持有不再需要的对象的引用,阻止垃圾收集器(或通过 ARC 释放)收集它们时,就会发生泄漏。典型原因:activity 销毁时未取消注册的事件 listener;持有 context 引用的单例;捕获 self 的闭包;添加数据而不删除的静态集合。堆转储提供了准确的图景:哪些对象是「存活的」,哪些是多余的,以及谁确切地引用了它们。

根据 Google I/O 的数据,超过 60% 的 Android 应用程序崩溃报告与 OutOfMemoryError 相关,并且在 80% 的情况下,根本原因是通过堆转储可检测的内存泄漏。对于 iOS 应用程序,情况类似:由 retain cycles 引起的泄漏是通过 Xcode 中的 Allocations 工具检测到的常见崩溃原因之一。

何时需要堆转储

出现以下症状时应执行堆转储:应用程序在重复操作(在屏幕之间来回导航)时线性消耗内存;屏幕工作结束后,内存未返回初始水平;出现 OutOfMemoryError 或 iOS 上的 memory warning 警告;应用程序因超出内存限制而终止(iOS 上的 EXC_RESOURCE_RESOURCE)。定期收集堆转储是大型移动项目(如 Instagram 和 Spotify)工程文化协议的一部分。

Android Studio 中的堆转储:获取和分析

Android Studio 提供 Memory Profiler——一个用于实时捕获堆转储的内置工具。通过 View → Tool Windows → Profiler 访问。启动应用程序后,选择会话,转到 Memory 选项卡,然后单击 Dump Java Heap。Android Studio 暂停应用程序,执行 ART 堆转储,并加载结果进行分析。转储文件的格式为 .hprof——与大多数内存分析器兼容的 HPROF 标准。

加载转储后,Android Studio 显示一个对象表,包含以下列:Allocations(实例数量)、Native Size(ART 堆之外的内存)、Shallow Size(对象本身的内存)、Retained Size(对象及其整个子图的内存)。按类名过滤、按 retained size 排序以及按包搜索可以快速找到问题区域。

kotlin
// 典型泄漏——未在 onDestroy 中取消注册的 listener
class MainActivity : AppCompatActivity() {
    private val sensorManager by lazy {
        getSystemService(SENSOR_SERVICE) as SensorManager
    }
    private val listener = MySensorListener()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        sensorManager.registerListener(listener,
            sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT),
            SensorManager.SENSOR_DELAY_NORMAL)
    }

    override fun onDestroy() {
        super.onDestroy()
        // ❌ 遗漏了 sensorManager.unregisterListener(listener)
        // → Activity 不会被 GC 收集,堆转储将显示泄漏
    }
}

Android Studio 中的 dominator tree 分析

Dominator Tree 选项卡显示持有最多内存的对象。如果从 dominator tree 中删除一个对象,它持有的所有内存都将可以被收集。这是关键工具:无需查看数千个对象,您只需关注控制 80-90% 内存的 10-20 个对象。根据 Google 的说法,dominator tree 分析是找到泄漏点的最有效方法,可将分析时间从数小时缩短到数分钟。

Xcode Instruments 中的堆转储:Allocations 和 Leaks

Xcode Instruments 提供两个用于处理堆转储的工具:Allocations——带有实时消耗图表的堆转储捕获;Leaks——通过 retain cycles 分析自动搜索泄漏。Allocations 显示堆中的所有对象、它们的大小、创建次数(allocations)和释放次数(deallocations)。特定类的创建次数和释放次数之间的差异表明存在潜在泄漏。

在 Allocations 中捕获堆转储通过 Snapshot Memory 按钮完成——该工具暂停应用程序并获取完整转储。之后,标准视图可用:按类别列出的对象列表、每个对象的调用树(call tree)和报告生成器。与 Android Studio 不同,Xcode 不使用 .hprof,而是以与 Instruments 兼容的自身 .trace 格式存储数据。

swift
// 典型 iOS 泄漏——通过闭包的 retain cycle
class NetworkManager {
    var onComplete: ((Data) -> Void)?

    func startRequest() {
        // ❌ 闭包捕获了 self —— retain cycle
        onComplete = { data in
            self.process(data)
        }
    }
    func process(_ data: Data) {}
}

Leaks 工具通过分析引用图自动检测 retain cycles 和泄漏。它用紫色图标标记泄漏对象,并显示到根(GC root)的路径。要消除 retain cycle,只需在闭包中添加 [weak self][unowned self]。在使用 Swift 进行 iOS 开发的团队中,定期运行 Leaks 工具是 CI 的强制阶段。

swift
// 修复——对 self 的弱引用
onComplete = { [weak self] data in
    guard let self else { return }
    self.process(data)
}

Shallow size、retained size 和 dominator tree

要正确分析堆转储,需要理解三个关键指标。Shallow size——对象直接占用的内存量:其字段、标头和对齐。对于典型的 Java/Kotlin 对象,shallow size 为 16-40 字节。Retained size——对象的 shallow size 加上仅通过此对象可访问的所有对象的总 shallow size(即删除时将成为垃圾)。正是 retained size 显示了对象对内存消耗的实际影响。

指标描述示例
Shallow size对象本身的字节大小Bitmap (100×100) = 40 016 B
Retained sizeShallow size + 它持有的所有内容带有 View Tree 的 Activity = 2-5 MB
Deep sizeRetained size + 来自其他图的嵌套对象带有适配器的 ScrollView = 10-50 MB

Dominator tree——一种结构,其中每个对象引用其「支配者」——控制其可达性的对象。如果支配者被删除,其子树的所有对象都将成为垃圾。Dominator tree 分析是找到持有最多内存的对象的最快方法。根据 Eclipse MAT(Memory Analyzer Tool)的数据,90% 的泄漏可以通过在 5 分钟内查看 top-20 dominator tree 来检测。

通过堆转储分析内存泄漏

通过堆转储分析泄漏的过程包括几个步骤。步骤 1:执行应该释放内存的操作(关闭屏幕、完成操作)。步骤 2:调用 GC(Android 上的 System.gc(),Xcode 中的强制 snapshot)并执行堆转储。步骤 3:查找应该已被销毁的对象(例如 finish 后的 Activity 实例)。步骤 4:对可疑对象执行 Path to GC Roots——使对象保持存活的引用链。链中的最后一个引用就是泄漏的原因。

通往 GC Roots 的路径(Path to GC Roots)

Path to GC Roots 函数在 Android Studio Profiler、Eclipse MAT 和 Xcode Instruments 中可用。它显示从 GC root 到问题对象的最短引用链。排除弱引用(weak)和软引用(soft)后,只留下强引用(strong)——那些真正阻止收集的引用。根据 Square Engineering 的统计数据,Android 应用程序中 70% 的泄漏仅由两种模式引起:对 Activity 或 Context 的静态引用,以及已注册但未取消注册的 listener。

kotlin
// 通过静态引用泄漏的示例
object AppCache {
    private val cache = mutableMapOf<String, Any>()

    fun storeActivityReference(activity: Activity) {
        cache["current_activity"] = activity // ❌ 泄漏!
    }
}

// 修复:弱引用
object AppCacheFixed {
    private val cache = mutableMapOf<String, WeakReference<Any>>()
}

比较两个堆转储

Comparison mode 技术——查找泄漏最有效的方法之一。在重复操作之前和之后进行堆转储(例如,五次导航到屏幕并返回)。比较关键类的实例数:如果 Activity 的数量增加了,尽管所有活动都已关闭——这就是泄漏。Android Studio 和 Eclipse MAT 支持自动比较转储并突出显示差异。根据 Google 的说法,由于效果的积累,比较转储可以发现在一次性分析中不可见的泄漏。

减少内存消耗的实用建议

基于实际项目中的堆转储分析,已经制定了经过验证的内存优化实践。使用 WeakReference 用于缓存、回调和长生命周期对象中对 context 的引用。取消注册 listener 在 Android 的 onPause/onDestroy 和 iOS 的 deinit 中。避免大型静态集合——如果必要,请使用具有大小限制的 LruCache。优化 Bitmap:使用正确的 inSampleSize 加载图像,使用 Glide 或 Picasso 配合磁盘缓存。

开发过程中的内存分析

将定期堆转储捕获纳入 CI。配置一个任务,运行 instrumentation UI 测试,执行关键用户场景,并将堆转储与基线进行比较。如果 retained size 比基线增加了超过 5%,则构建被标记为回归。这种方法在 Airbnb、Uber 和其他对质量要求高的公司中实行。根据 Uber Engineering 的数据,在 CI 中实施自动堆转储分析使与内存相关的 bug 数量在一个季度内减少了 70%。

groovy
// CI 中自动堆转储的 Gradle 任务示例
task profileMemory(type: Exec) {
    commandLine 'adb', 'shell',
        'am start -n com.example/.MainActivity'
    // 等待加载
    doLast {
        exec { commandLine 'adb', 'shell',
            'am broadcast -a com.example.DUMP_HEAP' }
    }
}

常见问题

Shallow size 和 retained size 有什么区别?

Shallow size——对象本身的大小(字段 + 标头)。Retained size——对象加上删除时将成为垃圾的所有对象的大小。Retained size 是对象对内存消耗影响的主要指标。

如何在物理 Android 设备上执行堆转储?

通过 Android Studio Profiler 选择设备和进程,单击 Dump Java Heap。或者通过命令行:adb shell am dumpheap PID /sdcard/dump.hprof,然后 adb pull

为什么堆转储可能非常巨大(500 MB+)?

堆转储包括所有存活对象。如果应用程序使用缓存、Bitmap 或处理大量数据,转储可能达到数百兆字节。按类过滤或使用 Eclipse MAT 仅加载索引。

可以在没有 Android Studio 的情况下分析堆转储吗?

可以,使用 Eclipse MAT(Memory Analyzer Tool)——一个用于分析 .hprof 的免费工具。支持 dominator tree、path to GC roots、转储比较以及通过 Leak Suspects Report 自动搜索泄漏。

堆转储会降低应用程序性能吗?

转储本身——是的,因为转储的收集会暂停所有线程(stop-the-world)。没有转储——不会。在受控条件下(测试环境、CI)进行转储,不要在生产环境中进行。

总结

  • Heap Dump——应用程序堆的完整快照,包含每个对象及其之间连接的信息。
  • Android Studio Memory Profiler 和 Xcode Instruments Allocations——捕获转储的主要工具。
  • Shallow size——对象本身的大小;retained size——对象及其整个依赖子图的大小。
  • Dominator tree——显示控制最多内存的对象的支配树。
  • Path to GC Roots——阻止对象被垃圾收集器收集的强引用链。
  • 比较两个堆转储(操作前/后)——检测泄漏最可靠的方法。
  • 在 CI 中自动化堆转储的捕获和分析可防止开发阶段的内存回归。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读