Skip to content

内存泄漏系统性排查 ​

泄漏排查的难点从来不是工具,而是纪律:用对比确认泄漏而不是看绝对值,用引用链定位而不是猜,用真机验证而不是只看实验室。这套方法论在 B 站 Web 端和小鹏车机端各跑通了一次。

fd 泄漏阈值> 20000
实测泄漏速度94.5MB/h
IGBP 上限4096

背景 ​

内存泄漏在两个完全不同的战场都遇到过:

  • B 站 Web 端:Three.js 虚拟形象换装,连续切换配件内存持续增长直至页面崩溃。
  • 小鹏车机端:SR 是长生命周期的车机进程,任何缓慢泄漏跑够时间都会变成线上事故——600 版本线上 crash 8844 例,610 版本更是演变成 P0 SR 黑屏/卡顿/自杀。

车机端更棘手的一点:凶手经常不在自己的进程里。surfaceflinger 的 fd 泄漏、IGBP 计数爆炸,表现都是 SR 被杀,根因却在系统侧的资源回收。

问题 ​

泄漏排查每一步都有坑:

  1. 确认难:内存涨不一定是泄漏,可能是缓存或延迟释放。
  2. 定位难:对象活着说明有人引用它,但「谁引用」要沿引用链一层层翻。
  3. 验证难:实验室环境和真机/实车的内存行为可能不同,修没修好要回到真实环境才算数。

方案 ​

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 pidresume 后 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 泄漏判据> 20000surfaceflinger fd 数超此值即判定泄漏
内存泄漏实测94.5MB/hsurfaceflinger 每小时泄漏量
IGBP 杀进程阈值4096超过即被系统杀
B 站换装验证连续换装 100 次内存稳定修复前持续增长至崩溃
600 版本 crash8844 例 → 三类根因分类后分批治理

复盘 ​

  • 确认泄漏用「操作 N 次后的增长对比」,绝对值大不等于泄漏——这条纪律在两个端都适用。
  • 进程被杀是保护结果,不是根因。watchdog 自杀、LMK 杀进程、IGBP 杀进程,根因都要往上游的资源链路找。
  • 车机端的泄漏很多时候是「别人家的内存」:surfaceflinger 的 fd 和 IGBP 爆了,死的是你的进程。监控要同时覆盖本进程和系统服务。
  • 排查经验必须沉淀成「关键词 → 根因类别」的速查表,否则每次都在重新发明排查路径。

罗子初 · AI 座舱专家 · 8年端侧研发

Lv.1