Appearance
内存泄漏系统性排查
泄漏排查的难点从来不是工具,而是纪律:用对比确认泄漏而不是看绝对值,用引用链定位而不是猜,用真机验证而不是只看实验室。这套方法论在 B 站 Web 端和小鹏车机端各跑通了一次。
fd 泄漏阈值> 20000
实测泄漏速度94.5MB/h
IGBP 上限4096
背景
内存泄漏在两个完全不同的战场都遇到过:
- B 站 Web 端:Three.js 虚拟形象换装,连续切换配件内存持续增长直至页面崩溃。
- 小鹏车机端:SR 是长生命周期的车机进程,任何缓慢泄漏跑够时间都会变成线上事故——600 版本线上 crash 8844 例,610 版本更是演变成 P0 SR 黑屏/卡顿/自杀。
车机端更棘手的一点:凶手经常不在自己的进程里。surfaceflinger 的 fd 泄漏、IGBP 计数爆炸,表现都是 SR 被杀,根因却在系统侧的资源回收。
问题
泄漏排查每一步都有坑:
- 确认难:内存涨不一定是泄漏,可能是缓存或延迟释放。
- 定位难:对象活着说明有人引用它,但「谁引用」要沿引用链一层层翻。
- 验证难:实验室环境和真机/实车的内存行为可能不同,修没修好要回到真实环境才算数。
方案
mermaid
flowchart TD
A["内存/资源异常"] --> B{"哪个环境?"}
B -->|"Web"| C["Heap Snapshot 对比<br>可疑操作重复 10 次"]
B -->|"车机"| D["DFX-SystemMonitor<br>盯 fd 数与内存趋势"]
C --> E["Retainers 查引用链<br>区分泄漏/缓存/延迟释放"]
D --> F{"fd > 20000?"}
F -->|"是"| G["fd 泄漏<br>Surface 未正确回收"]
F -->|"否"| H["看内存增速<br>与 IGBP 计数"]
E --> I["修复后重录对比<br>真机/实车验证"]
G --> I
H --> I关键实现
Web 端:Heap Snapshot 三步法
Step 1 确认:录制 Heap Snapshot → 可疑操作重复 10 次 → 再录一次
对比哪些对象数量持续增长(持续增长 ≠ 绝对值大)
Step 2 定位:对可疑对象看 Retainers 引用链,找到"谁在持有"
区分:真正的泄漏 vs 缓存 vs 延迟释放
Step 3 验证:修复后重做 Step 1 对比,确认数量不再增长,真机复验B 站换装案例:连续换装后 WebGLTexture 对象数量持续增长,Retainers 指向 Three.js 的 Motion Vector(mv pass)相关类。根因是换装时旧 Mesh 被移除,但关联的 WebGL 纹理资源没有级联释放。修复方式是改 Three.js 底层源码,在 Mesh dispose 时级联释放纹理。修复后连续换装 100 次内存稳定。
车机端:fd 与内存的量化判据
车机端没有 DevTools,靠系统监控和关键词:
| 根因类别 | 判据 / 关键词 | 说明 |
|---|---|---|
| fd 泄漏 | DFX-SystemMonitor: /system/bin/surfaceflinger,fd 数 > 20000 | 一般是 Surface 未正确回收 |
| 内存泄漏 | surfaceflinger 每小时泄漏 94.5MB | 渲染异常 JNI SurfaceTexture 相关 |
| IGBP 爆炸 | IGBP 计数超 4096 会触发系统杀进程 | 同上,Surface 回收问题 |
| 渲染线程卡死 | OpenGLRenderer、Choreographer: Skipped | 大屏日志看当前帧延迟 |
| 低内存被杀 | lowmemorykiller | 看自身与其他应用内存占用 |
| Unity GC 阻塞 | 多线程 block 在 GC_lock | 初步分析为 Unity 引擎 Bug |
| onResume 超时 | ScreenRootContainer: can not find surface, killing pid | resume 后 5s 内 surface 未创建/不可见即被杀 |
| hwbinder 拥堵 | kernel 日志搜 hwbinder、undelivered transaction_complete | 系统级 Binder 缓冲区耗尽 |
实际遇到过的根因远不止这些:Binder 通讯卡死、Binder 内存耗尽、hwbinder 拥堵、系统广播下发失败、Audio 接口卡死、SurfaceFlinger 卡驱动(setpowermode)、GPU kgsl 硬件故障(换件解决)——加起来十几类。每一类都有对应的日志关键词,排查时先按关键词分流,再深挖。
案例:600 版本 8844 例 crash 分类治理
面对线上 crash 列表,第一件事是分类而不是逐个看。8844 例归成三类根因:Unity 自杀(含 fd 泄漏)、渲染 SRP 崩溃、XStringBuilder 字符串溢出。配合自研的无限抓 trace 工具定位具体崩溃点,再实车拉日志验证内存——结论是 600 版本本身无内存泄漏,crash 多为第三方(百度)问题。分类的价值在于:每一类有不同的负责人和修法,混在一起看永远看不完。
指标
| 指标 | 数值 | 含义 |
|---|---|---|
| fd 泄漏判据 | > 20000 | surfaceflinger fd 数超此值即判定泄漏 |
| 内存泄漏实测 | 94.5MB/h | surfaceflinger 每小时泄漏量 |
| IGBP 杀进程阈值 | 4096 | 超过即被系统杀 |
| B 站换装验证 | 连续换装 100 次内存稳定 | 修复前持续增长至崩溃 |
| 600 版本 crash | 8844 例 → 三类根因 | 分类后分批治理 |
复盘
- 确认泄漏用「操作 N 次后的增长对比」,绝对值大不等于泄漏——这条纪律在两个端都适用。
- 进程被杀是保护结果,不是根因。watchdog 自杀、LMK 杀进程、IGBP 杀进程,根因都要往上游的资源链路找。
- 车机端的泄漏很多时候是「别人家的内存」:surfaceflinger 的 fd 和 IGBP 爆了,死的是你的进程。监控要同时覆盖本进程和系统服务。
- 排查经验必须沉淀成「关键词 → 根因类别」的速查表,否则每次都在重新发明排查路径。