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拦截关键分配点,只监控高频分配路径。
阈值设置的策略
阈值设高了,告警不及时;设低了,告警刷屏。我踩过这个坑,后来总结了一套策略:
- 静态阈值:基于设备内存容量设定固定值。比如2GB设备,Java Heap告警线设为160MB
- 动态阈值:根据App当前运行状态调整。比如在图片浏览页面,Bitmap阈值可以放宽
- 趋势阈值:监控内存增长速率。如果30秒内增长了50MB,即使没到绝对阈值也要告警
核心思路:阈值不是死的。我习惯用"三级告警"——黄色(注意)、橙色(警告)、红色(危险)。黄色靠趋势判断,红色靠绝对阈值。
线上问题排查实战
有了监控数据,怎么用它排查问题?我分享一个真实案例。
有一次线上反馈说某个版本OOM率飙升。我打开监控后台一看,发现Java Heap曲线在某个页面跳转时出现"锯齿状"——内存忽高忽低。这说明GC频繁触发,而且回收不彻底。
顺着这个线索,我定位到是一个自定义View在onDraw里创建了大量临时对象。每次绘制都new一个Paint和Path,导致GC压力巨大。
修复方案很简单:把Paint和Path提到成员变量里复用。上线后OOM率直接降了70%。
我的习惯:监控数据一定要可视化。光看数字很难发现问题,我一般把内存曲线和页面生命周期叠加在一起看。哪个页面跳转时内存暴涨,一目了然。
知识体系总览
下面这张图把内存监控的核心逻辑串起来了。从数据采集到告警触发,再到问题定位,每一步都有对应的策略。
一些实用技巧
最后分享几个我在实战中积累的小技巧:
- 采样频率别太高:30秒一次足够了。我曾经设成5秒一次,结果监控本身消耗了5%的CPU
- 带上上下文信息:上报内存数据时,同时带上当前页面、用户操作路径、设备型号。不然光看数字很难定位
- 本地缓存一份:网络不好时数据可能发不出去。我习惯在本地存最近100条记录,等网络恢复再补发
- 注意隐私合规:别采集用户敏感信息。内存数据本身没问题,但页面路径可能涉及隐私,记得脱敏
特别提醒:线上监控系统一定要有"熔断机制"。如果监控模块自身出现异常(比如OOM),要能自动关闭,不能影响主业务流程。我曾经见过监控模块崩溃导致App闪退的惨案。
好了,内存监控这块就聊到这儿。说白了,监控不是目的,目的是通过数据驱动优化。你想想看,没有数据支撑的优化,那不叫优化,叫碰运气。