26、内存监控与告警:自定义内存监控,阈值设置,线上问题排查

内存监控这事儿,说白了就是给App装个"血压计"。线上用户手机千奇百怪,光靠开发机上的Profiler根本不够。我见过太多App在测试环境跑得飞起,一上线上就各种OOM、卡顿。嗯,今天我们就聊聊怎么把内存监控做到位。

为什么需要自定义内存监控?

Android系统自带的工具,比如dumpsys meminfo、Profiler,都是"事后诸葛亮"。线上问题你总不能跑到用户手机上去抓吧?

我个人习惯的做法是:在App内部埋一套轻量级的内存采集系统。它不依赖adb,不依赖开发者工具,用户正常使用就能把数据传回来。

核心目标:实时感知App的内存水位,在崩溃前发出预警,在崩溃后能追溯现场。

监控哪些指标?

别什么都监控,抓重点。我一般关注这几个维度:

指标 说明 告警阈值(参考)
Java Heap Dalvik/ART堆内存使用量 超过最大堆的80%
Native Heap 通过malloc分配的内存 持续增长超过200MB
PSS 实际物理内存占用 超过设备总内存的30%
GC频率 每分钟GC次数 超过5次/分钟
Bitmap内存 图片占用的内存 超过总Heap的50%

我的经验:阈值别设太死。低端机和旗舰机的内存容量差好几倍。我习惯根据设备等级动态调整阈值,比如2GB以下设备阈值降低20%。

自定义监控的实现思路

代码怎么写?我直接给个核心框架。你想想看,监控系统无非就是三件事:采集、判断、上报。

public class MemoryMonitor {
    private static final String TAG = "MemoryMonitor";
    private static final long CHECK_INTERVAL = 30_000L; // 30秒检查一次
    
    private final Context context;
    private final MemoryThreshold threshold;
    private final MemorySampler sampler;
    
    public MemoryMonitor(Context context) {
        this.context = context.getApplicationContext();
        this.threshold = new MemoryThreshold();
        this.sampler = new MemorySampler();
    }
    
    public void start() {
        new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    MemorySnapshot snapshot = sampler.takeSnapshot();
                    if (threshold.isExceeded(snapshot)) {
                        reportAlert(snapshot);
                    }
                    Thread.sleep(CHECK_INTERVAL);
                } catch (InterruptedException e) {
                    break;
                }
            }
        }, "memory-monitor").start();
    }
    
    private void reportAlert(MemorySnapshot snapshot) {
        // 上报到你的监控平台
        Log.w(TAG, "内存告警: " + snapshot.toString());
        // 可以在这里触发dump hprof
    }
}

这段代码看着简单,但有几个坑要注意。我曾经在生产环境吃过亏——采集线程里做了太多IO操作,反而导致内存抖动。

避坑指南:采集操作一定要轻量。不要在监控线程里做文件写入、网络请求。我建议把采集和上报分离,采集线程只负责取数,上报交给异步队列。

如何获取准确的内存数据?

获取Java Heap很简单,Runtime.totalMemory()和Runtime.freeMemory()就能搞定。但Native内存就没那么容易了。

我常用的方法有几种:

  • Debug.getNativeHeapSize():ART运行时提供的API,能拿到Native堆的总大小
  • ActivityManager.getProcessMemoryInfo():获取PSS、USS等详细指标
  • 反射调用:有些厂商的ROM会隐藏一些调试接口,但兼容性不好

说实话,Native内存监控一直是个老大难。Android 8.0之后引入了Profiler的Native内存跟踪,但线上环境没法用。我现在的做法是:通过mallochook拦截关键分配点,只监控高频分配路径。

阈值设置的策略

阈值设高了,告警不及时;设低了,告警刷屏。我踩过这个坑,后来总结了一套策略:

  1. 静态阈值:基于设备内存容量设定固定值。比如2GB设备,Java Heap告警线设为160MB
  2. 动态阈值:根据App当前运行状态调整。比如在图片浏览页面,Bitmap阈值可以放宽
  3. 趋势阈值:监控内存增长速率。如果30秒内增长了50MB,即使没到绝对阈值也要告警

核心思路:阈值不是死的。我习惯用"三级告警"——黄色(注意)、橙色(警告)、红色(危险)。黄色靠趋势判断,红色靠绝对阈值。

线上问题排查实战

有了监控数据,怎么用它排查问题?我分享一个真实案例。

有一次线上反馈说某个版本OOM率飙升。我打开监控后台一看,发现Java Heap曲线在某个页面跳转时出现"锯齿状"——内存忽高忽低。这说明GC频繁触发,而且回收不彻底。

顺着这个线索,我定位到是一个自定义View在onDraw里创建了大量临时对象。每次绘制都new一个Paint和Path,导致GC压力巨大。

修复方案很简单:把Paint和Path提到成员变量里复用。上线后OOM率直接降了70%。

我的习惯:监控数据一定要可视化。光看数字很难发现问题,我一般把内存曲线和页面生命周期叠加在一起看。哪个页面跳转时内存暴涨,一目了然。

知识体系总览

下面这张图把内存监控的核心逻辑串起来了。从数据采集到告警触发,再到问题定位,每一步都有对应的策略。

内存监控与告警核心逻辑 数据采集层 Java Heap / Native / PSS 阈值判断层 静态 / 动态 / 趋势阈值 告警触发层 黄色 / 橙色 / 红色 问题定位与排查 内存曲线分析 / 页面关联 / 对象引用链 修复与验证 代码优化 / 资源复用 / 上线验证 持续优化 监控 → 判断 → 告警 → 定位 → 修复 → 再监控(闭环)

一些实用技巧

最后分享几个我在实战中积累的小技巧:

  • 采样频率别太高:30秒一次足够了。我曾经设成5秒一次,结果监控本身消耗了5%的CPU
  • 带上上下文信息:上报内存数据时,同时带上当前页面、用户操作路径、设备型号。不然光看数字很难定位
  • 本地缓存一份:网络不好时数据可能发不出去。我习惯在本地存最近100条记录,等网络恢复再补发
  • 注意隐私合规:别采集用户敏感信息。内存数据本身没问题,但页面路径可能涉及隐私,记得脱敏

特别提醒:线上监控系统一定要有"熔断机制"。如果监控模块自身出现异常(比如OOM),要能自动关闭,不能影响主业务流程。我曾经见过监控模块崩溃导致App闪退的惨案。

好了,内存监控这块就聊到这儿。说白了,监控不是目的,目的是通过数据驱动优化。你想想看,没有数据支撑的优化,那不叫优化,叫碰运气。

公众号:蓝海资料掘金营,微信deep3321