3、内存性能调优:内存管理机制、内存泄漏检测、LMK与OOM策略、ZRAM与Swap优化

内存调优,说白了就是让系统在有限的物理内存里,跑得又快又稳。我做了这么多年Android系统优化,最深的感触就是:内存问题往往是「温水煮青蛙」,等用户开始抱怨卡顿、闪退时,问题已经积累很久了。

这一章,咱们就聊聊内存管理的几个核心环节。我会结合自己踩过的坑,把内存机制、泄漏检测、LMK/OOM策略以及ZRAM和Swap的调优思路,掰开揉碎了讲清楚。

3.1 内存管理机制:你得先懂「游戏规则」

Android的内存管理,核心是「按需分配 + 低水位回收」。它不像Linux桌面系统那样有大量Swap空间,手机内存就那么几GB,用完了就得抢。

我个人习惯把Android内存管理分成三层来看:

  • 应用层:每个App有自己的Java堆、Native堆。GC(垃圾回收)是Java层的自动管家,但Native层得自己管。
  • 内核层:负责物理页面的分配与回收。这里的关键是kswapddirect reclaim
  • 框架层:AMS(ActivityManagerService)和LMK(Low Memory Killer)负责决定「谁该被干掉」。

嗯,这里要注意一个概念——内存压力。当系统可用内存低于某个阈值时,内核会启动回收机制。回收的顺序一般是:

  1. 回收文件缓存(Page Cache)
  2. 压缩匿名页(ZRAM)
  3. 杀掉进程(LMK/OOM)

我在项目中遇到过一个问题:某款手机在后台多开几个App后,切换应用时明显卡顿。查了一圈,发现是kswapd频繁唤醒,CPU占用飙升。说白了,就是内存回收太激进,导致CPU一直在做无用功。

调优小技巧:可以通过/proc/zoneinfo查看各内存区域的watermark水位线。如果kswapd唤醒太频繁,可以适当调高watermark_scale_factor,减少回收频率。

3.2 内存泄漏检测:别让「小漏洞」变成「大窟窿」

内存泄漏,说白了就是对象用完了没释放,GC又没法回收。Java层泄漏常见于静态变量持有Activity、Handler未移除回调、注册监听未注销等。Native层泄漏就更隐蔽了,比如JNI分配的内存没释放。

我曾经接手过一个项目,系统跑几天后内存占用从2GB涨到4GB,最后直接OOM。查了三天,发现是一个后台服务在循环创建Bitmap,但recycle()方法没调用。嗯,这种问题用工具查其实很快。

我常用的检测手段有这些:

工具/方法 适用场景 我的使用心得
LeakCanary Java层泄漏 集成简单,但线上环境慎用,有性能开销
MAT (Memory Analyzer) 离线分析Heap Dump 适合深挖泄漏根因,但需要一定经验
AddressSanitizer (ASan) Native层泄漏/越界 Google官方推荐,但会拖慢运行速度
dumpsys meminfo 快速查看进程内存 我经常在adb shell里直接跑,看PSS和Native Heap

举个例子,用dumpsys meminfo排查泄漏:

# 查看某个进程的内存详情
adb shell dumpsys meminfo com.example.app

# 重点关注这几项:
# Native Heap: 如果持续增长,大概率是Native泄漏
# Dalvik Heap: 如果GC后仍不下降,Java层可能有泄漏
# .so mmap: 动态库映射,如果异常增长,检查JNI代码
避坑指南:我曾经在线上环境直接集成LeakCanary,结果发现它在低端机上导致GC频繁触发,反而加剧了卡顿。建议只在Debug版本使用,或者用更轻量的hprof采样方案。

3.3 LMK与OOM策略:谁该「牺牲」?

LMK(Low Memory Killer)是Android特有的内存回收机制。它不像Linux的OOM Killer那样随机杀进程,而是根据进程的「重要性」来排序。你想想看,如果系统把正在播放音乐的后台进程杀了,用户肯定骂娘。

LMK的决策依据是oom_score_adj,这个值越低,进程越重要。系统预设了几个层级:

  • -1000:系统核心进程(如system_server),不可杀
  • 0:前台进程(用户正在交互的App)
  • 100:可见进程(如悬浮窗)
  • 200:可感知进程(如后台音乐播放)
  • 900~1000:缓存进程(被杀优先级最高)

我在项目中遇到过一个问题:某款手机在内存紧张时,总是先杀掉用户刚切到后台的App,导致「杀后台」现象严重。查了LMK配置,发现minfree参数设置得太激进。

调优LMK,核心是调整/sys/module/lowmemorykiller/parameters/minfree。这个文件里是一串数字,对应不同优先级的内存阈值。举个例子:

# 默认配置(单位:页,1页=4KB)
# 格式:minfree_adj, minfree_adj, ...
# 对应优先级:0, 100, 200, 300, 900, 1000
echo "18432,23040,27648,32256,55296,80640" > /sys/module/lowmemorykiller/parameters/minfree

# 调优思路:适当降低缓存进程的阈值,避免误杀
# 比如把最后两个值调低到 46080, 69120
调优建议:我个人习惯先跑一轮压力测试(比如同时打开20个App),然后用dmesg查看LMK的杀进程日志。如果发现某个重要进程被误杀,就调高它的oom_score_adj,或者降低对应阈值的minfree

至于OOM(Out Of Memory),它是LMK的「最后防线」。当LMK来不及回收时,内核会触发OOM Killer。但OOM Killer的杀进程逻辑比较粗暴——它倾向于杀掉占用内存最大的进程。这可能导致系统核心进程被误杀,引发重启。

嗯,这里有个经验:尽量让LMK在OOM之前完成回收。如果系统频繁触发OOM,说明LMK的阈值设置不合理,或者内存泄漏太严重。

3.4 ZRAM与Swap优化:用压缩换空间

ZRAM,说白了就是用CPU压缩内存中的数据,腾出更多可用空间。它不像传统的Swap(需要写磁盘),而是直接在内存里划出一块区域做压缩。你想想看,压缩后的数据可能只有原来的1/3到1/2,相当于白嫖了2倍内存。

但ZRAM不是万能的。压缩和解压需要CPU时间,如果压缩率太低或者CPU太弱,反而会拖慢系统。我遇到过一款低端机,开启ZRAM后,系统响应变慢了——因为CPU一直在忙着压缩解压。

调优ZRAM,主要关注这几个参数:

参数 说明 我的推荐值
disksize ZRAM设备的大小 物理内存的25%~50%
comp_algorithm 压缩算法(lzo/lz4/zstd) lz4(平衡速度与压缩率)
max_comp_streams 最大并发压缩流数 CPU核心数

举个例子,配置一个2GB的ZRAM设备:

# 创建ZRAM设备
echo "2G" > /sys/block/zram0/disksize
mkswap /dev/block/zram0
swapon /dev/block/zram0

# 查看压缩情况
cat /sys/block/zram0/mm_stat
# 输出示例:
# orig_data_size: 1048576  (原始数据大小)
# compr_data_size: 524288  (压缩后数据大小)
# 压缩率 = compr_data_size / orig_data_size ≈ 50%
避坑指南:我曾经在4GB内存的机器上设置了3GB的ZRAM,结果发现系统频繁触发Swap,CPU占用率飙升。后来把ZRAM降到1.5GB,配合LMK调优,效果反而更好。记住:ZRAM不是越大越好,要留足给前台进程的「干净内存」。

至于传统的Swap(写到闪存),我个人不建议在Android上开启。闪存的读写速度比内存慢几个数量级,而且频繁写入会缩短闪存寿命。除非是特殊场景(比如低端机内存实在不够),否则还是优先用ZRAM。

小结

内存调优,说到底就是「平衡」二字。平衡内存回收的激进程度,平衡压缩带来的CPU开销,平衡不同进程的优先级。我见过太多团队只盯着一个指标调,结果按下葫芦浮起瓢。

最后分享一个我的习惯:每次调完内存参数,我都会跑一轮「压力测试 + 稳定性测试」。压力测试用stressapptestmemtester,稳定性测试就是正常使用一周,观察有没有异常重启或卡顿。嗯,实践出真知,调优没有银弹。