系统低内存的数据和行为特征

本文探讨下系统低内存时的一些数据和行为特征

发布日期 2022-06-07

前言

经常在分析稳定性anr之类的问题,很多同事看见lmk有关的日志就认为是lmk导致,但不一定和自己的应用Anr异常有关,故此集了下低内存的相关知识,会对分析anr问题分析是否和lmk相关有帮助。

Meminfo信息

最简单的方法是使用 Android 系统自带的 Dumpsys meminfo 工具,通过使用dumpsys
adb shell dumpsys meminfo 如果系统处于低内存的话 , 会有如下特征:
FreeRam 的值非常少 , Used RAM 的值非常大
ZRAM 使用率非常高(如果开了 Zram 的话)

LMK && kswapd 线程活跃

低内存的时候,LKMD 会非常活跃,在 Kernel Log 里面可以看到 LMK 杀进程的信息: 上面这段 Log 的意思是说, 由于 mem 低于我们设定的 900 的水位线 (261272kB),所以把 pid 为 15609 的 mzsyncservice 这个进程杀掉(这个进程的 adj 是 906 )

proc/meminfo

这里是 Linux Kernel 展示 meminfo 从结果来看 , 当系统处于低内存的情况时候 , MemFree 和 MemAvailable 的值都很小。

低内存对性能的具体影响

一般当低内存时,会出现很多IO问题,cpu 负载高,进程频繁查杀和重启等,启动慢。

影响主线程 IO 操作
Linux 系统的 page cache 链表中有时会出现一些还没准备好的 page ( 即还没把磁盘中的内容完全地读出来 ) , 而正好此时用户在访问这个 page 时就会出现 wait_on_page_locked_killable 阻塞了. 只有系统当 io 操作很繁忙时, 每笔的 io 操作都需要等待排队时, 极其容易出现且阻塞的时间往往会比较长.
当出现大量的 IO 操作的时候,应用主线程的 Uninterruptible Sleep 也会变多,此时涉及到 io 操作(比如 view ,读文件,读配置文件、读 odex 文件),都会触发 Uninterruptible Sleep , 导致整个操作的时间变长,如通过systrace查看:

出现 CPU 竞争

低内存会触发 Low Memory Killer 进程频繁进行扫描和杀进程,kswapd0 是一个内核工作线程,内存不足时会被唤醒,做内存回收的工作。 当内存频繁在低水位的时候,kswapd0 会被频繁唤醒,占用 cpu ,造成卡顿和耗电。 比如下面这个情况, kswapd0 占用了 855 的超大核 cpu7 ,而且是满频在跑,耗电可想而知,如果此时前台应用的主线程跑到了 cpu7 上,很大可能会出现 cpu 竞争,导致调度不到而丢帧。 HeapTaskDaemon 通常也会在低内存的时候跑的很高, 来做内存相关的操作:

进程频繁查杀和重启

对 AMS 的影响主要集中在进程的查杀上面 , 由于 LMK 的介入 , 处于 Cache 状态的进程很容易被杀掉 , 然后又被他们的父进程或者其他的应用所拉起来 , 导致陷入了一种死循环 . 对系统 CPU \ Memory \ IO 等资源的影响非常大.比如下面就是一次 Monkey 之后的结果 , QQ 在短时间内频繁被杀和重启。

其对应的 Systrace - SystemServer 中可以看到 AM 在频繁杀 QQ 和起 QQ 此 Trace 对应的 Kernel 部分也可以看到繁忙的 cpu

影响内存分配和触发 IO

手机经过长时间老化使用整机卡顿一下 , 或者整体比刚刚开机的时候操作要慢 , 可能是因为触发了内存回收或者 block io , 而这两者又经常有关联 . 内存回收可能触发了 fast path 回收 \ kswapd 回收 \ direct reclaim 回收 \ LMK杀进程回收等。(fast path 回收不进行回写) 回收的内容是匿名页 swapout 或者 file-backed 页写回和清空。(假设手机都是 swap file 都是内存,不是 disk), 涉及到 file 的,都可能操作 io,增加 block io 的概率。 还有更常见的是打开之前打开过的应用,没有第一次打开的快,需要加载或者卡一段时间 . 可能发生了 do_page_fault,这条路径经常见到 block io 在 wait_on_page_bit_killable(),如果是 swapout 内存,就要 swapin 了。如果是普通文件,就要 read out in pagecache/disk. do_page_fault —> lock_page_or_retry -> wait_on_page_bit_killable 里面会判断 page 是否置位 PG_locked, 如果置位就一直阻塞, 直到 PG_locked 被清除 , 而 PG_locked 标志位是在回写开始时和 I/O 读完成时才会被清除,而 readahead 到 pagecache 功能也对 block io 产生影响,太大了增加阻塞概率。

低内存的启动情况

低内存情况下 , 如下systrace这个 App 从 bindApplication 到第一帧显示 , 共花费了 2s . 从下面的 Thread 信息那里可以看到: Uninterruptible Sleep | WakeKill - Block I/O 和 Uninterruptible Sleep 这两栏总共花费 750 ms 左右(对比下面正常情况才 130 ms) Running 的时间在 600 ms (对比下面正常情况才 624 ms , 相差不大) 从这段时间内的 CPU 使用情况来看 , 除了 HeapTaskDeamon 跑的比较多之外 , 其他的内存和 io 相关的进程也非常多 , 比如若干个 kworker 和 kswapd0.

正常内存情况下

正常内存情况下 , App 从 bindApplication 到第一帧显示 , 只需要 1.22s . 从下面的 Thread 信息那里可以看到 Uninterruptible Sleep | WakeKill - Block I/O 和 Uninterruptible Sleep 这两栏总共才 130 ms. Running 的时间是 624 ms 从这段时间内的 CPU 使用情况来看 , 除了 HeapTaskDeamon 跑的比较多之外 , 其他的内存和 io 相关的进程非常少.

一般性能方面的优化策略

1,提高 extra_free_kbytes 值减少GC
2,提高 disk I/O 读写速率,如用 UFS3.0,用固态硬盘
3,避免设置太大的 read_ahead_kb 值
4,使用 cgroup 的 blkio 来限制后台进程的 io 读操作,缩短前台 io 响应时间
5,提前做内存回收的操作,避免在用户使用应用时碰到而感受到稍微卡顿
6,增加 LMK 效率,避免无效的 kill kswapd 周期性回收更多的 high 水位
7,调整 swappiness 来平衡 pagecache 和 swap
8,针对低内存机器做特殊的策略 , 比如杀进程更加激进 (这会带来用户体验的降低 , 所以这个度需要兼顾性能和用户体验)
9,在内存不足的时候提醒用户(或者不提醒用户) , 杀掉不必要的后台进程 .