查找LookWorldPro内存泄漏:先固定复现场景并长时间重放,持续记录内存曲线与堆快照,结合分配火焰图和堆栈,找出持续增长的对象与持有链;优先排查计时器、回调、监听、缓存、单例及本地资源,借助LeakCanary/Instruments/Profiler定位,定位后小步修复回归验证检测纳入CI。


为什么要按步骤查:先把问题变成可观察的
遇到内存泄漏直觉往往是“哪里分配了太多”,但*直觉不可复现*。第一要务是把泄漏变成可观测、可重放的现象:固定操作路径、频率与持续时间,这样才能对比前后堆情况、判定是否真的泄漏而非短时峰值。
准备工作(很关键)
- 确定环境:稳定的测试设备/模拟器、相同的启动参数、禁用不相关插件。
- 复现场景脚本化:用脚本或自动化工具连续重复用户操作,保证长时间运行得到增长趋势。
- 记录基线:启动后等待若干次GC,采集初始堆快照与内存曲线,作为对比。
- 选择合适工具:不同平台不同工具(下文有表),先从高层监控到堆分析再到本地诊断。
复现与监控:看“谁在持续增长”
思路是先宏观再微观。先用内存曲线确认长期增长趋势,再采集两次或多次堆快照做差分。必要时同时导出分配火焰图或 allocation tracing,能把“哪个方法频繁分配”暴露出来。
常用做法
- 持续记录:内存曲线、线程数、句柄数、类的加载数。
- 对比快照:启动后、复现前、复现中、复现后分别采集堆快照。
- 重复验证:每次改动都用相同脚本重放并采集数据,避免偶发问题误导。
常见泄漏模式(按经验优先顺序)
- 未注销的监听/回调:事件总线、广播、全局监听器忘记remove/ unregister。
- 长生命周期的引用:静态变量、单例持有短生命周期对象(Context、Activity等)。
- 计时器/定时任务:Timer、ScheduledExecutor、Handler延迟消息未取消。
- 集合/缓存无限增长:没有边界或过期策略的缓存/Map。
- 闭包与Lambda:匿名内部类/闭包意外捕获外部大对象。
- 线程局部与线程泄漏:ThreadLocal未清理、线程池线程持有引用。
- 本地(native)资源:JNI创建的全局引用、malloc未释放、Bitmap/byte[]没回收。
平台与工具速查表
| 平台 |
快速定位 |
深入分析 |
| Android |
Android Studio Profiler、LeakCanary |
Heap dump + MAT(Eclipse Memory Analyzer)、Allocation Tracker |
| iOS |
Xcode Instruments(Allocations、Leaks) |
Malloc Stack Logging、Address Sanitizer(ASan) |
| JVM 服务端 |
jvisualvm、jcmd GC/heap info |
jmap + MAT、VisualVM、YourKit |
| 本地 C/C++ |
Valgrind、ASan |
heaptrack、massif(gperftools)、malloc hooks |
实际定位流程(费曼法:把复杂事情讲简单)
把问题拆成“可视化→归因→验证→防回归”四步:
1) 可视化:证明有泄漏
- 用业务脚本长时运行(例如连续打开页面、刷新列表、下载、播放),观察内存曲线是否平稳或持续上升。
- 同时监控GC频率、响应时间,确认不是垃圾收集时机造成的误判。
2) 归因:从类/对象到持有链
- 采集多个时点的堆快照,比较“增长最多”的类(retained size/objects)。
- 对疑似类查看保留路径(retaining path),这一步能直接告诉你是哪条链把对象绑着。
- 若是频繁分配造成的累积,查看分配火焰图和分配调用栈,找出高频分配点。
3) 验证:缩小范围与复测
- 在代码中临时添加日志打印或弱引用检测,确认对象是否按预期被回收(比如用System.gc()观察堆变化,但别当生产手段)。
- 用二分法禁用/注释某块功能,看内存曲线是否改善,逐步定位具体模块。
4) 修复与回归:小步快跑,防止引入新问题
- 改动尽量小且针对性强:优先在释放时解除监听、取消任务、清理缓存。
- 每次修改后都用脚本重放并采集堆快照,确认问题解决且无新增长点。
- 把自动检测集成进CI(例如在Nightly跑堆快照比对、或用轻量级LeakCheck)。
针对性排查技巧与陷阱(实战口味)
- 计时器与Handler:Android中Handler.postDelayed或TimerTask忘clear是常见罪魁,优先查看消息队列是否还有未处理消息。
- Listener与Adapter:Activity被Adapter或长生命周期对象持有,容易导致Activity泄漏,检查Adapter是否持有Context引用。
- 缓存策略:用弱引用或LruCache并设置合理最大容量,避免无限增长。
- 闭包捕获:Lambda或匿名内部类捕获外部this时,改为静态内部类+弱引用或显式传入需要的数据。
- 线程与池:线程未结束或线程池保持活动线程会导致ThreadLocal或上下文被长期持有,定期清理ThreadLocal。
- 本地内存:Bitmap、DirectByteBuffer、native malloc等不会被JVM/ART自动回收,检查native释放逻辑与全局引用。
示例检查清单(方便贴到任务单)
| 步骤 |
检查项 |
| 环境 |
是否可复现脚本化;设备/版本一致性 |
| 监控 |
是否记录内存曲线、GC频率、句柄/线程数 |
| 堆分析 |
是否采集多时点堆快照并做对比 |
| 怀疑点 |
列出候选类/对象与持有链 |
| 修复 |
小步改动→重放→快照比对→PR注释 |
工具使用的小提示
- 堆快照别只看实例数:优先看retained size(保留大小),它更能反映“如果释放这个对象,会释放多少内存”。
- 火焰图要和堆快照搭配:火焰图告诉你哪里在分配,堆快照告诉你哪些对象没被回收,两者结合能快速找到罪魁。
- 不同版本的对比:若怀疑回归引入泄漏,把新旧版本在相同脚本下的堆快照做diff,差距处通常就是起点。
最后:把检测变成流程的一部分
修掉一次泄漏只是临时胜利,回归风险存在。把内存稳定性纳入代码评审要点、自动化测试与Nightly检测,能把未来的问题早早发现。还有一点,排查时别只信单张快照,多点位、多轮次比对结果会更可靠——这就是为什么我在实际操作中喜欢把脚本跑得久一点,让问题“慢慢露出马脚”。