Android 端稳定性压测:内存泄漏自动检测系统设计(上)——为什么传统方案不靠谱

24 阅读7分钟

这是「Android 内存泄漏自动检测」系列的第 1 篇,共 3 篇。 本篇聚焦:传统内存泄漏检测的 5 个致命问题、整体架构设计、采集层如何做到 50ms 级低干扰采集。


压测 3 天,上线就崩?

不知道你有没有经历过这样的场景——稳定性压测跑了整整 3 天,测试报告上写着"内存正常"。结果上线不到两周,用户反馈 App 越用越卡,最后 OOM 崩溃。

复盘发现,压测确实跑了,但内存泄漏检测还停留在**"测完了对比首末内存"**的原始阶段。3 天的压测过程中没有任何实时监控,内存从 200MB 慢慢涨到 800MB,全程无人感知。

这不是个例。跟不少做测试的同行交流,大多数团队的内存泄漏检测都面临同样的困境:

痛点说一个大家都懂的场景
发现滞后压测跑了 3 天,测完才发现泄漏。但此时进程已经重启过了,现场没了
诊断困难知道泄漏了,但没有过程数据。只能再跑一遍,蹲在旁边手动抓 hprof
类型不分不管 Java 泄漏还是 Native 泄漏还是 GPU 泄漏,统一抓 hprof——但 hprof 只能看 Java 堆,对其他类型完全无效
采集太重每 17 秒跑一次 dumpsys meminfo,每次占 system_server 5~15 秒,采集行为本身就在拖慢 App
多进程超时5 个进程串行 dumpsys 要 50~75 秒,直接超过采集周期

一句话总结核心矛盾:你想高频采集以便尽早发现泄漏,但传统的 dumpsys 方式太重了,频率一高就反过来干扰被测应用。


我想做到什么效果

压测跑着的时候,工具自动发现泄漏、判断泄漏类型、抓对应的诊断文件。测完直接给开发一份可分析的文件,不需要人盯。

拆开来说:

  • 采集要轻——高频采集不超过 100ms,不影响被测 App
  • 检测要准——不需要人工配阈值,20 MB/h 的慢泄漏也能抓到
  • 诊断要对症——Java 泄漏抓 hprof,Native 泄漏抓 showmap,GPU 泄漏抓 gfxinfo
  • 多进程要稳——监控 10 个进程也不超时

整体设计:三层流水线

整个方案分三层,形成 "采数据 → 判泄漏 → 抓文件" 的完整链路:

graph TB
    subgraph CJ ["采集层"]
        H["高频轻量通道<br>smaps_rollup 或 smaps awk<br>每 15-30s, 耗时 50ms-3s<br>产出: PSS 总量"]
        L["低频详细通道 - 轮转制<br>dumpsys meminfo<br>每 30s 轮转 1 个进程, 耗时 5-15s<br>产出: 8 维度 + TOTAL PSS + GPU 校准"]
    end

    subgraph JC ["检测层"]
        A1["线性回归 + t 检验 + R2<br>斜率显著性判断"]
        A2["P25 基线递增确认<br>底线水位持续上涨 = 泄漏"]
        A3["多维度交叉验证<br>8 维度独立回归, 泄漏分类"]
        SM["四级状态机<br>NORMAL - SUSPICIOUS - CONFIRMING - LEAKING"]
        AUX["辅助路径: 突增检测, GPU 泄漏检测"]
    end

    subgraph XY ["响应层"]
        RT["分类诊断路由<br>java: hprof / native: showmap<br>gpu: gfxinfo / thread: proc-task"]
        CD["冷却控制: 每进程 30min + 全局锁"]
    end

    H -->|"PSS 时间序列"| A1
    L -->|"8 维度数据"| A3
    A1 --> SM
    A2 --> SM
    A3 --> SM
    AUX --> SM
    SM -->|"泄漏类型"| RT
    RT --> CD

三层各干一件事

  • 采集层:两条通道并行。高频通道读内核文件拿 PSS 总量(50ms),低频通道轮流对每个进程 dumpsys 拿 8 维度明细
  • 检测层:线性回归 + t 检验判趋势,P25 基线确认不是虚涨,四级状态机逐步升级证据
  • 响应层:根据泄漏类型走不同诊断路径——只有 Java 泄漏才抓 hprof,其他用轻量命令

本篇重点展开采集层。检测层和响应层分别在后续两篇详解。


采集层设计:拆通道解决核心矛盾

思路:把"看趋势"和"看明细"分开

内存泄漏检测需要高频数据——数据点越多,统计检验灵敏度越高,慢泄漏才能被发现。但每次都用 dumpsys meminfo 采完整明细,单次耗时 5~15 秒,频率一高 system_server 就扛不住。

解法是拆成两条通道

通道采什么怎么采多久一次耗时用途
高频PSS 总量(1 个数字)读内核文件 /proc/{pid}/smaps_rollup15-30 秒50ms趋势检测
低频8 维度明细dumpsys meminfo轮转,每进程 90 秒+5-15 秒泄漏分类

高频通道直接读 Linux 内核文件,完全不经过 system_server,一个数字足以做线性回归。低频通道走 dumpsys 拿 Java Heap / Native Heap / Graphics 等 8 个维度的详细数据,但不需要高频——只在确认泄漏后用来分类。

两条通道独立运行,互不干扰。

三级设备适配

不是所有设备都支持 smaps_rollup。Android 10 以上才有这个内核文件,老设备需要降级方案。我们在设备首次连接时做一次自动探测:

flowchart TD
    A["设备连接"] --> B{"读取 smaps_rollup<br>成功且含 Pss ?"}
    B -->|"是"| L1["Level 1: Android 10+<br>耗时 约50ms, 直读内核"]
    B -->|"否"| C{"读取 smaps 前 20 行<br>成功且含 Pss ?"}
    C -->|"是"| L2["Level 2: Android 5-9<br>耗时 1-3s, awk 聚合"]
    C -->|"否"| L3["Level 3: 兜底<br>仅 dumpsys meminfo, 5-15s"]
    L1 --> CACHE["结果缓存, 后续直接使用"]
    L2 --> CACHE
    L3 --> CACHE
Level适用版本高频方式耗时原理
1Android 10+cat smaps_rollup~50ms内核预聚合的 PSS 汇总,绕过 system_server
2Android 5-9awk '/^Pss/' smaps1-3s从 VMA 列表手动聚合,同样不走 system_server
3极老设备无高频,仅低频回退到 dumpsys,牺牲灵敏度保证兼容

探测结果缓存到设备维度,后续所有采集直接按对应方式执行。

高频通道:自适应采集频率

高频通道并不是固定频率的。它会根据检测层状态机的当前阶段自动调速——正常巡逻时慢一点省资源,发现可疑时加快积累数据:

状态采集间隔说明
NORMAL(巡逻)30 秒低开销,够发现中快速泄漏
SUSPICIOUS(可疑)15 秒加密采集,t 检验灵敏度提高约 2.8 倍
LEAKING(已确认)60 秒减少干扰,此时在做诊断操作

采到的数据以 (timestamp, pss) 元组存入固定大小的滑动窗口 deque(maxlen=240),窗口满了自动淘汰最老的数据,内存占用恒定。

关键细节:数据带了真实时间戳。即使采集间隔从 30s 变为 15s(加密采集),线性回归以时间为 x 轴,斜率计算不受变频影响。

低频通道:轮转制解决多进程超时

这是解决多进程超时的核心设计。

传统做法:每 30 秒对 5 个进程全部串行 dumpsys → 总耗时 25~75 秒 → 超时。

轮转制:每 30 秒只对 1 个进程 dumpsys,下次换下一个 → 单次永远只要 5~15 秒 → 永不超时

sequenceDiagram
    participant Timer as 低频定时器 每30s
    participant A as 进程A
    participant B as 进程B
    participant C as 进程C

    Timer->>A: dumpsys meminfo A 约10s
    Note over Timer: t = 0s
    Timer->>B: dumpsys meminfo B 约8s
    Note over Timer: t = 30s
    Timer->>C: dumpsys meminfo C 约12s
    Note over Timer: t = 60s
    Timer->>A: dumpsys meminfo A 约10s
    Note over Timer: t = 90s 回到A
对比项串行全采轮转制
3 进程单轮耗时15-45s(可能超时)5-15s(永不超时)
5 进程单轮耗时25-75s(大概率超时)5-15s
10 进程单轮耗时50-150s(必定超时)5-15s
system_server 峰值负载连续高负载均匀分布

代价是每个进程的详细数据间隔变成了 30s × 进程数,但低频数据只用于泄漏分类,不用于趋势检测,这个频率完全够用。

GPU 校准:别漏了显存

一个容易忽略的盲区:smaps 看不到 GPU 显存

GPU 纹理和帧缓冲区由 GPU 驱动通过 DMA-BUF 或 ION 机制分配,不在进程虚拟地址空间内。如果只看高频 PSS,纯 GPU 泄漏完全检测不到。

解决方法:每次低频 dumpsys 时,从 App Summary 中提取 Graphics 字段值作为 GPU 偏移量:

校准后 PSS = smaps_pss + Graphics 字段值

Graphics 值和 TOTAL PSS 来自同一次 dumpsys 调用,时间严格对齐。泄漏检测看的是增长趋势(斜率),不是绝对值,所以即使 GPU 偏移量在两次低频采集之间有微小变化,也不影响趋势判断。


小结

采集层通过双通道 + 三级适配 + 轮转制 + GPU 校准,解决了"高频 vs 开销"的核心矛盾:

能力效果
高频 PSS 采集50ms 一次,不走 system_server
多进程支持10 个进程也不超时
设备兼容Android 5 到最新版本自动适配
GPU 覆盖校准后 PSS 包含 GPU 显存

但拿到数据只是第一步。一条波动的 PSS 曲线,怎么判断它到底是在泄漏,还是只是正常的业务抖动?


下篇预告

中篇:用统计学替代"拍脑袋阈值"

传统方案的做法是"切两半比均值,差值超过 50MB 就算泄漏"——这个 50MB 是怎么来的?拍脑袋。

下一篇讲检测层如何用线性回归 + t 检验 + P25 基线 + 四级状态机,做到零配置阈值、自适应不同进程的波动特征。


系列目录

  • 本篇(上):为什么传统方案不靠谱 + 采集层设计
  • 中篇:用统计学替代"拍脑袋阈值"——检测层设计
  • 下篇:对症下药,5 种泄漏 5 种抓法——响应层设计

我是测试工坊,专注自动化测试和性能工程。 如果觉得有用,点个赞让更多人看到 👍 关注我,后续更新不迷路。