在Android 中的卡顿丢帧原因概述 - 系统篇 这篇文章中, 实际案例这里我们有列举一些由于系统低内存导致的卡顿, 由于 Android 低内存对整机性能影响比较大, 所以单独写一篇文章, 来概述系统低内存对整机性能的影响 .
随着 Android 系统版本的更迭, 以及 App 的代码膨胀, Android 系统对内存的需求越来越大, 在写作当年的 2019 年,市面上还存在着大量 4GB 内存及以下的机器, 这部分用户就很容易遇到整机低内存的情况, 尤其是在系统大版本更新、同时运行的应用和服务增多的情况下 .
Android 内存压力可能导致性能问题, 具体表现就是响应慢和卡顿 . 比如启动一个应用要花比平时更长的时间 ; 滑动列表会掉更多帧 ; 后台的进程减少导致冷启动变多 ; 额外回收和重启工作也可能增加功耗,但发热本身不能证明低内存, 下面我会概述发生这些性能问题的原因 . Debug 的方法, 以及可能的优化措施 .
版本说明(2026-10-13 技术复核): 本文保留 2019 年的日志与 Systrace 案例。旧内核 LMK 日志、调参接口与函数名有版本限制;下文补充 Android 10/11 的 PSI/lmkd、Android 16 框架源码和 Linux 6.6 的回收机制。历史截图没有完整原始 Trace 和受控实验条件,修订只校正可见证据,不把它们当成当前设备的复测结果。
- 0. Android 中的卡顿丢帧原因概述 - 方法论
- 1. Android 中的卡顿丢帧原因概述 - 系统篇
- 2. Android 中的卡顿丢帧原因概述 - 应用篇
- 3. Android 中的卡顿丢帧原因概述 - 低内存篇
低内存的数据特征和行为特征
Meminfo 信息
可以先用 Android 的 dumpsys meminfo 查看系统汇总,再结合进程和时间序列分析:
1 | adb shell dumpsys meminfo |
以下保留原文历史输出,其中 156,ba480K 已损坏,不应猜成一个有效数值:
1 | ...... |
这段数据不能直接作为“低内存必然如此”的判据:
- Android 9 的 AMS 汇总逻辑将 cached PSS、部分内核缓存和实际空闲页合在
Free RAM中;它不等于/proc/meminfo的MemFree,也不等于MemAvailable。 Used RAM和Lost RAM涉及 PSS、swap PSS、内核及 ZRAM 的不同统计口径。这里Used RAM大于Total RAM、Lost RAM为负,应回到对应版本和原始采样核查,不能解释成物理内存真的“负剩余”,也不能据此认定泄漏。status moderate同样是框架状态,不是逐帧压力测量。2,608K physical used、301,256K in swap和4,247,544K total swap分别描述物理占用、已用交换空间和交换空间容量,不能混算成同一个“ZRAM 使用率”。仅这组数字也不支持原文“ZRAM 使用率非常高”的结论。
缓存占用不是坏事。应观察可用内存是否持续下降、回收和换入是否造成停顿,以及同一时段的前台响应;单次“空闲很少”不足以诊断低内存卡顿。
LMK && kswapd 线程活跃
下面是旧内核 lowmemorykiller 驱动的日志,不是现代用户空间 lmkd 的通用日志格式:
1 | [kswapd0] lowmemorykiller: Killing 'u.mzsyncservice' (15609) (tgid 15609), adj 906, |
它记录了在 kswapd0 的回收上下文中选择 PID 15609、adj 906 的进程作为清理对象,并列出 cache 258652kB、阈值 261272kB 等条件。不能把它改读成“所有系统都配置了 adj 900 水位”;-5540kB above reserved 是扣除保留条件后的差值,不是物理空闲页数为负。确切阈值和筛选方式需要该厂商内核源码。
kswapd 是内核后台页回收线程;旧 LMK 驱动和用户空间 lmkd 是不同实现,也不要与 Linux OOM killer 或 AMS 清理缓存/空进程混在一起。看到 kswapd 活跃,不等于已经发生杀进程。
版本更新: AOSP lmkd 文档说明,Android 10 支持用内核 PSI 监视压力,Android 11 的策略进一步考虑内存使用与 page-cache thrashing。是否启用仍取决于内核支持和产品配置,不能只看 Android 版本号。现代排查要读取对应版本的 lmkd 原因和压力指标,不应只搜索旧 lowmemorykiller: 字符串。
proc/meminfo
内核汇总可以这样读取,注意路径前的 /:
1 | adb shell cat /proc/meminfo |
原始样本如下;它的 MemTotal 与上一个 dumpsys 样本不同,不能当作同一设备、同一时刻的联合快照。
1 | MemTotal: 5630104 kB |
按照 Linux 6.6 /proc 文档,MemFree 是空闲 RAM,MemAvailable 是考虑可回收缓存及保留水位后、不发生交换时可用于新应用的估计。上面两者分别为 148928 kB 和 864172 kB,不能统一概括为“都几乎为零”。SwapFree 只剩 42576 kB,说明这个快照的交换空间已大量使用,但不证明此刻正在频繁换入换出。Committed_AS 也不是当前物理驻留量。
需要进一步定位时,在设备允许读取、内核提供接口的前提下,可以结合:
1 | adb shell cat /proc/vmstat |
vmstat 应看同一次复现前后的增量,例如回收扫描/回收成功、allocstall*、pswpin/pswpout 和 workingset_refault*,具体字段依内核而异;累计值大不等于刚刚卡顿。PSI 的 some 表示至少部分任务受阻,full 表示所有非空闲任务同时受阻;avg10/60/300 是相应秒数窗口的时间比例,total 为累计微秒。详见 PSI 文档。接口不存在、权限不足或未采集时记为“不可用”,不能补成零压力。
整机卡顿 && 响应慢
内存压力可能表现为启动慢、滑动掉帧和更多冷启动,但这些现象也可能由 CPU、存储、锁或其他负载引起。先把卡顿时间与压力、回收、线程等待、进程退出记录对齐,再判断内存是否在这次场景的关键路径上。
低内存对性能的具体影响
影响主线程 IO 操作
内存压力会增加回收工作,可能挤掉后续还要访问的文件缓存,也可能带来交换相关工作。如果前台线程因此等待页面或存储完成,就会延长响应时间。
Trace 上要区分 Uninterruptible Sleep 与带 Block I/O 的分类,并查看阻塞调用点。D 状态或黄色本身不是磁盘 I/O 的充分证据;wait_on_page_bit_killable 表示等待相关页状态,不独证低内存、具体文件或存储设备饱和。缓存命中时,读文件也不一定触发这种等待,加载 View 更不必然发生磁盘读取。
这张 SystemUI 主线程图的可见阻塞点是 io_schedule,支持继续追查 I/O 等待,但没有给出同一时刻的内存压力值:

下一张是 OPScreenRecord 的 bindApplication,包含 APK/OAT 加载区间和线程状态;它提示启动阶段存在等待,不能只凭颜色确定每一段等待的原因。

出现 CPU 竞争
后台回收、压缩/解压以及重新加载内容会消耗 CPU。当这些工作与前台争用运行机会时,可能增加 Runnable 等待;要结合调度轨道确认,而不能只看有同名线程出现。
下图能看到 kswapd0 多次在 CPU 7 上运行。原文将设备描述为骁龙 855,但这张裁剪图没有足够的拓扑和数值频率信息,不能单凭 CPU 编号证明“超大核满频”,也没有测出功耗或某个前台线程被它延误的时间。

HeapTaskDaemon 执行 ART 堆任务;它的活动不等于整段都是 GC,更不等于整段停世界。是否与系统压力有关,需要具体 GC/堆任务和应用分配证据,而不是从线程名推导。
进程频繁查杀和重启
频繁退出和重启会增加分配、代码加载及 I/O 工作,但要先区分谁杀了进程、为什么重启。下面保留当时 Monkey 测试后的原始日志:
1 | 07-23 14:32:16.932 1435 1510 I am_proc_start: [0,30387,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq] |
这里九轮 am_kill 的原因都是 empty #3。在 AOSP Android 9 AMS中,空进程数量超过限额会以 empty # 加计数的形式清理;Android 16 的 OomAdjuster仍有对应的 too many empty 路径。因此,这组日志支持“框架反复清理空进程,随后又发生重启”,不能作为 LMK 直接杀 QQ 的证据。restart 标签也不能独自证明是父进程拉活,需要继续追组件、服务重启或其他启动请求。
对应 system_server 图中可见重复的启动和 killProcessGroup 活动;这不是单一杀进程原因的专用图形:

下图显示 CPU 上密集的运行切片,但缺少完整线程统计,不能把全部 CPU 消耗归到 QQ 或 LMK。这里也不评价这些应用的当前版本。

影响内存分配和触发 IO
手机用了很长时间,再打开一个之前用过的应用,反而没有第一次快,还要等它加载一会儿——这也是我关注低内存卡顿的一个场景。不过,光凭这个现象还不能判断原因。往下查时,需要区分几个环节,而不是把它们都叫作“回收”:
- 分配快路径尝试从已有空闲页满足请求;不应称为“fast path 回收”。不足时,慢路径可能唤醒
kswapd、进行直接回收或内存规整,具体取决于分配标志和条件。直接回收在请求分配的线程上下文中执行,可能直接影响前台延迟。参见 Linux 6.6page_alloc.c。 - 回收文件页不一定写盘。干净、可回收的文件缓存可被丢弃,之后再访问才可能重新读入;脏页通常涉及写回,不能把所有 file-backed 页都描述成“先写回再清空”。
- 匿名页与 swap也不等于存储读写。ZRAM 在 RAM 中压缩存储,仍占物理内存和 CPU;配置了 backing device/writeback 时又可能涉及实际存储。
/proc/swaps和设备配置决定了具体路径,不能假设所有手机 swap 都是纯 RAM。参见 Linux 6.6 ZRAM 文档。 - 缺页不一定需要读盘;已在页缓存/swap cache 中的数据、首次映射等情况与需要存储 I/O 的缺页不同。重新打开应用变慢也可能是进程已被杀而冷启动,不要只归为 swap-in。
旧内核中的 do_page_fault、lock_page_or_retry、wait_on_page_bit_killable 是分析入口示例,不是所有架构和版本都相同的固定调用链。Linux 6.6 的文件缓存路径已大量使用 folio 接口。PG_locked 与 PG_writeback 是不同状态;对应源码中的 folio_unlock() 与 folio_end_writeback()分别处理解锁和写回结束。原文“页锁只在开始回写或读 I/O 完成时清除”的概括不成立,killable 等待还可能受致命信号影响。
预读过大可能增加无用 I/O 和缓存挤占,过小也可能减少顺序访问收益。是否需要调小,应该由访问模式、命中率和延迟决定。
实例
下面是当时抓的两份启动截图,我分别标为低内存和正常内存。箭头选择的是 bindApplication 到首帧附近,不是从用户点击开始的完整启动耗时,也没有独立的实际呈现时间证明。两次的温度、缓存、后台任务等条件未完整提供,不能当作只改变内存这一因素的受控对照。
低内存的启动情况
上方箭头为 2.044 s,下方 Thread Timeslices 统计范围为 2053.451 ms,两者略有不同。按下方表格逐行读取:Running 是 682.137 ms;带 Block I/O 的两项合计 647.450 ms,全部四项 Uninterruptible 状态合计 783.385 ms。原文“750ms 左右”只概括了部分行,不能替代完整分类。

CPU Slices 列表可见 HeapTaskDaemon、多个 kworker、kswapd0,但线程名汇总可能合并不同进程的同名线程,且时间跨 CPU 累计,不等于主线程等待时间或整段墙钟时间。

正常内存情况下
正常样本上方箭头为 1.223 s,下方线程统计范围为 1227.232 ms。Running 为 624.278 ms,全部三项 Uninterruptible 合计 139.417 ms,其中带 Block I/O 的两项为 33.097 ms。

两张截图的线程状态表整理如下,单位均为毫秒;不将箭头范围与表格范围混算:
| 项目 | 低内存样本 | 正常样本 |
|---|---|---|
| 统计范围 | 2053.451 | 1227.232 |
| Running | 682.137 | 624.278 |
| Runnable | 63.060 | 43.902 |
| Sleeping | 524.869 | 419.635 |
| Uninterruptible(全部子类合计) | 783.385 | 139.417 |
| 其中带 Block I/O 的子类 | 647.450 | 33.097 |
最后一行已经包含在前一行,不能再相加。低内存样本增加最多的是 Uninterruptible 状态,但其中没有 Block I/O 标记的部分不能全部算成磁盘 I/O;这些截图也不足以把差值全部归因于低内存。
正常样本的 CPU 列表里仍有 HeapTaskDaemon 和其他工作线程。低内存列表显示 kswapd0,正常截图的可见行没显示它;“没在截图排名中出现”不等于该时段完全没有回收。两次选区长度不同,不能只比较总 CPU 毫秒数就认定某个线程工作强度更高。

可能的优化方案 (来自实际的经验和大佬分享的经验)
下面这些方向来自我自己的经验和同行分享,具体问题还是要具体分析。平台调参属于系统/OEM 工作;普通应用应先管理自己的分配、缓存和后台任务。 有常驻通知不等于一定占用大量内存,也不是无条件清理进程的理由。减少进程数量可能增加冷启动、丢失工作和后台功能中断,目标应是前台响应与功能可靠性,而不是“空闲 RAM 越大越好”。
- 水位调整:
extra_free_kbytes是历史 Android 扩展接口,不能假设所有内核都存在。AOSP Android 16 的适配脚本会检查该节点,缺失时计算watermark_scale_factor;两者对 high 水位的影响并不相同,不能逐值替换。调高可能减少分配突发时的直接回收,也可能增加后台回收、减少可用的缓存空间。 - 存储: UFS 3.0 是当年的硬件选型例子,不是软件修复或充分性能保证。看实际随机 I/O、队列、尾延迟和热状态,手机也不能按桌面电脑的方式简单“换固态硬盘”。
- 预读: 根据顺序/随机访问和缓存命中率调整
read_ahead_kb,不提供统一的越小越好数值。 - 后台 I/O: 根据设备采用的 cgroup 版本和控制器做隔离;v1 的
blkio与 v2 的io.max等接口不同。限流过度也可能延误前台依赖的后台工作,见 Linux 6.6 cgroup v2 文档。 - 提前回收: 可以在已验证的空闲阶段试验,但要计入 CPU、压缩、写回和之后 refault 的成本;不要以周期性清缓存代替容量管理。
- 减少无效退出/重启: 先分清 lmkd、AMS 数量限制、崩溃与应用启动请求,修具体循环;“杀得更多”不等于效率更高。
- kswapd: 围绕水位与分配突发调节,观察
allocstall*等数据;它不是按固定周期必定回收到更多 high 水位的通用策略。 - swappiness: 在 Linux 6.6 中是 swap 与文件分页的相对 I/O 成本偏好,范围
0–200,不是“内存使用达到多少百分比才换出”。ZRAM 场景也不应机械设为 0;选择值要实测。 - 低 RAM 产品策略: 在同等场景下比较前台尾延迟、冷启动率、退出率、功耗和后台功能成功率,再决定缓存/进程保留策略;不要仅按 RAM 容量复制另一台设备的参数。
- 应用侧: 修泄漏,给位图/对象缓存设容量边界,减少无用分配和不必要的后台任务,将允许异步的 I/O 移出主线程并避免主线程等待其结果。Android 14 / API 34 起,普通应用不能用
killBackgroundProcesses()清理其他包,见桌面被杀案例的版本更新。 - 无法恢复的异常: 重启可以是用户选择的临时恢复手段,不能算根因修复;在可行时保留异常日志、压力和进程数据,继续查泄漏或异常负载。
这些参数没有通用最优值。验证优化时应固定设备/构建、负载、缓存和热状态,重复采集并看分布,避免把一次截图的改善当作已解决问题。
参考资料
当前机制与版本依据已在正文就近链接。以下保留原文历史参考,不作为当前版本行为的唯一依据:
本文知乎地址
由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎界面
知乎 - Android 中的卡顿丢帧原因概述 - 低内存篇
关于我 && 博客
下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!
- 博主个人介绍 :里面有个人的微信和微信群链接。
- 本博客内容导航 :个人博客内容的一个导航。
- 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
- Android性能优化知识星球:付费社群已于 2026 年停止运营,不再招收新成员;此处保留历史介绍。
一个人可以走的更快 , 一群人可以走的更远
