3、内存性能调优:内存管理机制、内存泄漏检测、LMK与OOM策略、ZRAM与Swap优化
内存调优,说白了就是让系统在有限的物理内存里,跑得又快又稳。我做了这么多年Android系统优化,最深的感触就是:内存问题往往是「温水煮青蛙」,等用户开始抱怨卡顿、闪退时,问题已经积累很久了。
这一章,咱们就聊聊内存管理的几个核心环节。我会结合自己踩过的坑,把内存机制、泄漏检测、LMK/OOM策略以及ZRAM和Swap的调优思路,掰开揉碎了讲清楚。
3.1 内存管理机制:你得先懂「游戏规则」
Android的内存管理,核心是「按需分配 + 低水位回收」。它不像Linux桌面系统那样有大量Swap空间,手机内存就那么几GB,用完了就得抢。
我个人习惯把Android内存管理分成三层来看:
- 应用层:每个App有自己的Java堆、Native堆。GC(垃圾回收)是Java层的自动管家,但Native层得自己管。
- 内核层:负责物理页面的分配与回收。这里的关键是
kswapd和direct reclaim。 - 框架层:AMS(ActivityManagerService)和LMK(Low Memory Killer)负责决定「谁该被干掉」。
嗯,这里要注意一个概念——内存压力。当系统可用内存低于某个阈值时,内核会启动回收机制。回收的顺序一般是:
- 回收文件缓存(Page Cache)
- 压缩匿名页(ZRAM)
- 杀掉进程(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代码
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
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%
至于传统的Swap(写到闪存),我个人不建议在Android上开启。闪存的读写速度比内存慢几个数量级,而且频繁写入会缩短闪存寿命。除非是特殊场景(比如低端机内存实在不够),否则还是优先用ZRAM。
小结
内存调优,说到底就是「平衡」二字。平衡内存回收的激进程度,平衡压缩带来的CPU开销,平衡不同进程的优先级。我见过太多团队只盯着一个指标调,结果按下葫芦浮起瓢。
最后分享一个我的习惯:每次调完内存参数,我都会跑一轮「压力测试 + 稳定性测试」。压力测试用stressapptest或memtester,稳定性测试就是正常使用一周,观察有没有异常重启或卡顿。嗯,实践出真知,调优没有银弹。