Heap Dump(堆转储)——应用程序动态内存的快照,包含所有存活对象的完整信息:它们的类、大小、相互引用以及从根节点(GC roots)的可达性。Heap Dump 是分析内存泄漏和优化资源消耗的主要工具。根据 Android Developers,堆转储分析可以检测多达 95% 的内存泄漏,包括循环引用、被遗忘的 listener 以及未释放的静态引用。
要点
堆转储是虚拟机堆的完整转储——所有动态创建的对象所在的内存区域。在 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 提供 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 排序以及按包搜索可以快速找到问题区域。
// 典型泄漏——未在 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 收集,堆转储将显示泄漏
}
}
Dominator Tree 选项卡显示持有最多内存的对象。如果从 dominator tree 中删除一个对象,它持有的所有内存都将可以被收集。这是关键工具:无需查看数千个对象,您只需关注控制 80-90% 内存的 10-20 个对象。根据 Google 的说法,dominator tree 分析是找到泄漏点的最有效方法,可将分析时间从数小时缩短到数分钟。
Xcode Instruments 提供两个用于处理堆转储的工具:Allocations——带有实时消耗图表的堆转储捕获;Leaks——通过 retain cycles 分析自动搜索泄漏。Allocations 显示堆中的所有对象、它们的大小、创建次数(allocations)和释放次数(deallocations)。特定类的创建次数和释放次数之间的差异表明存在潜在泄漏。
在 Allocations 中捕获堆转储通过 Snapshot Memory 按钮完成——该工具暂停应用程序并获取完整转储。之后,标准视图可用:按类别列出的对象列表、每个对象的调用树(call tree)和报告生成器。与 Android Studio 不同,Xcode 不使用 .hprof,而是以与 Instruments 兼容的自身 .trace 格式存储数据。
// 典型 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 的强制阶段。
// 修复——对 self 的弱引用
onComplete = { [weak self] data in
guard let self else { return }
self.process(data)
}
要正确分析堆转储,需要理解三个关键指标。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 size | Shallow size + 它持有的所有内容 | 带有 View Tree 的 Activity = 2-5 MB |
| Deep size | Retained 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——使对象保持存活的引用链。链中的最后一个引用就是泄漏的原因。
Path to GC Roots 函数在 Android Studio Profiler、Eclipse MAT 和 Xcode Instruments 中可用。它显示从 GC root 到问题对象的最短引用链。排除弱引用(weak)和软引用(soft)后,只留下强引用(strong)——那些真正阻止收集的引用。根据 Square Engineering 的统计数据,Android 应用程序中 70% 的泄漏仅由两种模式引起:对 Activity 或 Context 的静态引用,以及已注册但未取消注册的 listener。
// 通过静态引用泄漏的示例
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%。
// 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——对象加上删除时将成为垃圾的所有对象的大小。Retained size 是对象对内存消耗影响的主要指标。
通过 Android Studio Profiler 选择设备和进程,单击 Dump Java Heap。或者通过命令行:adb shell am dumpheap PID /sdcard/dump.hprof,然后 adb pull。
堆转储包括所有存活对象。如果应用程序使用缓存、Bitmap 或处理大量数据,转储可能达到数百兆字节。按类过滤或使用 Eclipse MAT 仅加载索引。
可以,使用 Eclipse MAT(Memory Analyzer Tool)——一个用于分析 .hprof 的免费工具。支持 dominator tree、path to GC roots、转储比较以及通过 Leak Suspects Report 自动搜索泄漏。
转储本身——是的,因为转储的收集会暂停所有线程(stop-the-world)。没有转储——不会。在受控条件下(测试环境、CI)进行转储,不要在生产环境中进行。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。