Android内存回收:GC、kswapd 和 mm_vmscan_direct_reclaim概述

Android内存回收:GC、kswapd 和 mm_vmscan_direct_reclaim概述

摘要:Android系统中的三种内存管理机制。Java/ART GC负责回收Java堆内存,而kswapd是内核后台异步回收线程。当内存不足时,应用线程可能被迫执行direct reclaim(mm_vmscan_direct_reclaim),在内核态同步回收内存。这种现象不同于GC的全局暂停,而是特定线程的阻塞式回收。解释了为何在有GC和kswapd的情况下仍会发生direct reclaim,主要压力源是Native Heap、Bitmap等非Java堆内存。当主线程等待执行direct reclaim的后台线程时,就会导致UI卡顿。三种机制形成分层管理体系,共同维护系统内存平衡。

GC、kswapd 和 mm_vmscan_direct_reclaim 解决的是不同层面的内存问题。即使有 Java/ART GC,也即使 kswapd 在后台回收,应用线程仍然可能在申请内存时被迫进入 mm_vmscan_direct_reclaim

并且:

mm_vmscan_direct_reclaim 不是发生在 Java 层,也不是图库业务代码内部;它是 Linux/Android 内核的内存回收路径。但它可以发生在应用的某个线程上下文中。此时这个线程会暂停执行自己的用户态任务,转而在内核态同步做内存回收。

1. 为什么有 Java GC,还会发生 direct reclaim?

因为 Java GC 只管理 Java/ART 堆里的对象,而应用的大量内存并不完全在 Java Heap 里。

例如:

复制代码
Java Heap:       约 49 MB
Native Heap:     alloc 约 503 MB
Graphics/GL:     约 376 MB
Bitmap:          约 201 MB
SwapPss:         约 395 MB

这说明应用主要压力来自:

  • Native Heap

  • Bitmap native allocation

  • GL texture / Graphics buffer

  • gralloc / dma-buf

  • 图片 decode 中间 buffer

  • native 图片库内存

  • page cache

  • zram/swap

  • 文件映射

  • 可能还有图像 AI / 编辑 / 缩略图模块

这些内存很多 不是 Java GC 能直接释放的

1.1 Java GC 只能判断 Java 对象是否可达

ART GC 负责类似这些对象:

复制代码
ImageView
List
Bitmap wrapper Java object
Gallery item model
Adapter
Activity/Fragment/View

但很多真实大内存在 native/kernel/graphics 层。比如一个 Bitmap,Java 层可能只是一个小对象,真正像素内存可能在 native heap 或 graphics memory 中。

再比如 GL 纹理:

复制代码
Java 对象可能已经很小
但 GL texture / gralloc buffer / dmabuf 可能占用几十到几百 MB

Java GC 不一定能立即释放这些资源,尤其当:

  • Java 对象仍然被引用;

  • native 层还有引用;

  • GL 资源未显式释放;

  • 图形资源等待渲染管线释放;

  • BitmapPool / LruCache 仍持有;

  • native allocator 没有把 free memory 归还给内核;

  • 页面只是被释放到 malloc arena 内部,没有 madvise 给内核。

所以即使 Java GC 正常工作,系统层仍可能缺页、缺空闲页、缺连续页,触发 direct reclaim。

2. 为什么有 kswapd,还会发生 mm_vmscan_direct_reclaim

kswapd 是后台回收线程。它的职责是提前回收内存,把各个 zone 的 free pages 维持在一定水位之上。

但它不是万能的。

当某个应用线程申请内存时,如果发现当前可用页不够,或者 zone 水位不足,或者高阶连续页不足,内核就可能让这个申请内存的线程自己进入回收流程。

这个就是:

复制代码
direct reclaim

大致逻辑是:

复制代码
应用线程申请内存
        ↓
内核检查 free pages / watermark / zone / order
        ↓
发现当前不能满足分配
        ↓
kswapd 可能已经在跑,但还不够
        ↓
当前申请线程被迫自己回收内存
        ↓
进入 mm_vmscan_direct_reclaim

所以:

复制代码
kswapd = 后台异步回收
direct reclaim = 当前申请内存的线程同步回收

2.1 kswapd 后台异步内存回收跟不上应用所需

复制代码
14:44:26.315
PSI stat: 12, 22.59, 8.63, 9.88, 3.82
kswapd reclaim amount : 51218 pages
kswapd reclaim efficiency : 335 KB/ms

14:44:27.398
PSI stat: 13, 22.59, 8.63, 9.88, 3.82
kswapd reclaim amount : 44418 pages
kswapd reclaim efficiency : 236 KB/ms

14:44:28.479
PSI stat: 13, 31.35, 10.67, 13.89, 4.74
kswapd reclaim amount : 36186 pages
kswapd reclaim efficiency : 229 KB/ms

这说明:

  • kswapd 确实在大量回收;

  • PSI 仍然在升高;

  • 回收效率在下降;

  • 系统内存 stall 仍在加重。

也就是说:

不是没有 kswapd,而是 kswapd 已经很忙,但仍然无法及时满足前台应用线程的内存分配需求,于是 前台应用的线程 进入了 direct reclaim。

3. mm_vmscan_direct_reclaim 是发生在应用进程内部吗?

这个问题要分两层说。

3.1 从代码归属看:不是应用进程代码,是内核代码

mm_vmscan_direct_reclaim 属于 Linux kernel 内存管理模块,大概在:

复制代码
mm/vmscan.c

它不是 应用进程 的 Java/Kotlin/C++ 业务代码。

所以不能说:

应用进程代码里调用了 mm_vmscan_direct_reclaim

更准确是:

应用进程的线程在执行某个内存申请、page fault、buffer 分配或 native allocation 时进入内核,内核为了满足该分配,在该线程上下文中执行了 mm_vmscan_direct_reclaim

3.2 从线程上下文看:发生在应用线程上下文中

trace 里显示:

复制代码
应用的线程
  mm_vmscan_direct_reclaim

这意味着:

应用线程进入了内核态,并且在它自己的线程上下文里执行 direct reclaim。

所以 CPU trace 会把这段时间归到 应用 线程上。

可以理解为:

复制代码
应用的线程 本来要做图片加载/解码/分配内存
        ↓
调用 malloc / mmap / decode / Bitmap allocate / page fault / buffer allocate
        ↓
陷入内核
        ↓
内核发现内存分配压力大
        ↓
让 应用的线程 这个线程帮系统做回收
        ↓
应用的线程 在内核态跑 mm_vmscan_direct_reclaim
        ↓
回收完成或失败后才返回用户态继续应用的任务

所以更准确的表述是:

mm_vmscan_direct_reclaim 是内核函数,但它发生在应用的线程上下文中。

4. direct reclaim 发生时,会像 GC 一样停止线程任务吗?

对当前线程来说:会。

对整个进程来说:不一定

对整个系统来说:更不是 stop-the-world。

4.1 对当前线程:它会暂停原本任务

当 应用线程 进入 direct reclaim 后,它不能继续执行自己的加载逻辑。

它原本可能在做:

复制代码
加载图片
解码 Bitmap
申请 native buffer
上传纹理
构建缩略图缓存
通知主线程

但进入 direct reclaim 后,它实际在做:

复制代码
扫描 LRU page
回收 file cache
回收 slab
尝试回收匿名页
可能触发 swap/zram
等待 IO
等待锁
执行 shrinker
可能参与 compaction

所以从业务角度看:

应用的线程 的业务任务被暂停了。

这就是为什么主线程等不到它的结果。

4.2 但它不像 Java GC 的 Stop-The-World

Java/ART GC,特别是某些阶段,会暂停所有 Java mutator 线程,或者暂停一部分线程。它是运行时层面的对象回收。

而 direct reclaim 是内核内存分配路径上的同步回收。

区别如下:

项目 Java/ART GC direct reclaim
所属层级 ART Runtime Linux Kernel
管理对象 Java Heap 对象 系统物理页、page cache、slab、匿名页、swap 等
触发原因 Java Heap 需要回收 内核分配内存时水位不足
是否停止当前线程 是,可能暂停 Java 线程 是,当前申请内存线程会被阻塞在内核
是否停止整个进程 某些 GC 阶段可能 STW 通常不会停止整个进程
是否停止其他线程 GC 可能影响多个 Java 线程 只阻塞进入 direct reclaim 的线程,其他线程可继续运行
trace 表现 GC、suspend、art 等 mm_vmscan_direct_reclaimshrink_nodetry_to_free_pages
典型耗时 通常 ms 级,严重时几十/上百 ms 严重内存压力下可达数百 ms 甚至秒级

所以 direct reclaim 不是 GC,但对发生它的线程来说,效果很像:

"线程原本任务被迫暂停,去做内核内存回收。"

5. 为什么主线程 Sleeping,而不是主线程自己 direct reclaim?

复制代码
Android应用的主线程发起
        ↓
主线程等待 后台线程 返回数据或通知
        ↓
后台线程 执行线程加载任务
        ↓
后台线程 申请内存
        ↓
后台线程 进入 mm_vmscan_direct_reclaim
        ↓
后台线程 长时间无法完成任务
        ↓
主线程一直 Sleeping
        ↓
后台线程 终于完成并唤醒主线程

所以用户看到的是主线程卡,但直接陷入 reclaim 的是 后台线程。

这也是为什么 trace 显示:

复制代码
主线程 Sleeping
Woken by 后台线程
后台线程 mm_vmscan_direct_reclaim

这条链非常典型。

6. direct reclaim 期间线程可能具体卡在哪里?

mm_vmscan_direct_reclaim 不是一个简单函数,它背后可能包括很多事情。

典型路径可能类似:

复制代码
__alloc_pages_slowpath
  -> try_to_free_pages
    -> do_try_to_free_pages
      -> shrink_zones
        -> shrink_node
          -> shrink_lruvec
            -> shrink_list
            -> shrink_inactive_list
            -> shrink_active_list

期间可能发生:

  • 扫描大量 LRU page;

  • 回收文件页 cache;

  • 回收匿名页;

  • 把匿名页换出到 zram/swap;

  • 等待 page writeback;

  • 等待 page lock;

  • 执行 slab shrinker;

  • 执行 filesystem shrinker;

  • 执行 dmabuf / ion / graphics 相关 shrinker;

  • 触发 compaction;

  • 被更高优先级线程抢占;

  • 因 PSI 统计记录 stall。

所以 trace 里看到:

复制代码
Runnable (Preempted)
mm_vmscan_direct_reclaim

是合理的。

它可能一会儿在内核态扫描,一会儿被抢占,一会儿等待锁或 IO,一会儿继续扫描。

7. GC、kswapd、direct reclaim 三者的关系可以这样理解

可以用一个分层模型理解:

复制代码
Java/ART GC
  负责清理 Java 层不用的对象
  ↓
Native allocator / Bitmap / GL / Cache
  负责释放 native 和图形资源
  ↓
Kernel kswapd
  后台异步回收系统页
  ↓
Kernel direct reclaim
  当某个线程申请内存但系统无法立即满足时,
  让该线程同步参与回收
  ↓
LMKD
  如果回收仍无法缓解,可能杀后台进程

所以它们不是互相替代关系,而是不同层次的机制。

复制代码
Java GC 不足以释放主要压力
        ↓
图库 Native/Bitmap/GL 占用高
        ↓
kswapd 后台回收已经很忙
        ↓
但系统仍然压力大,PSI 升高
        ↓
LoadMgr-7 申请内存时进入 direct reclaim
        ↓
主线程等待 LoadMgr-7,导致卡顿

8. 一个非常重要的点:kernel 不知道 Java 对象是否"没用了"

Java GC 能判断对象是否可达:

复制代码
某个 Bitmap Java 对象是否还有引用
某个 List 是否还被 Activity 持有
某个 ImageView 是否还引用 Drawable

但内核看不到这些语义。

内核看到的是:

复制代码
这个进程有一些匿名页
这些页是否在 LRU 上
是否 mapped
是否 dirty
是否 locked
是否可 swap
是否可 reclaim
是否 pinned

所以如果应用进程的业务层仍然持有 Bitmap/GL/cache 引用,或者 native 层资源没有释放,那么:

复制代码
Java GC 不会释放
kernel 也不能随便回收

结果就是系统只能:

  • 回收 file cache;

  • swap 匿名页;

  • 回收 slab;

  • 尝试 shrinker;

  • 触发 direct reclaim;

  • 甚至最后 LMK kill 进程。

这就是为什么应用进程高 Bitmap/GL/Native 占用非常容易导致内存回收型卡顿。

9. 最准确的解释

应该这样理解:

复制代码
1. 应用冷启动/恢复后,线程T 开始加载图片、解码、分配 Bitmap/native/GL 资源。

2. 此时系统内存状态不好:
   MemFree 低,SwapUsed 高,CmaFree 为 0,
   lmkd_mm 中 PSI 升高,kswapd 高强度回收且效率下降。

3. kswapd 已经在后台回收,但赶不上应用线程的内存申请速度,
   或者遇到 zone/CMA/碎片/局部 memcg 压力。

4. 线程T 在申请内存时进入内核 slow path,
   由内核在 线程T 上下文中执行 mm_vmscan_direct_reclaim。

5. direct reclaim 期间,线程T 不能继续执行加载任务,
   等价于这个线程T 被同步阻塞在内核回收里。

6. 主线程正在等待 线程T 的加载结果或通知,
   因此主线程 Sleeping 1820ms+。

7. 线程任务T 从 direct reclaim 返回并完成部分任务后,
   唤醒主线程,trace 显示 Woken by 线程T。

10. 总结

ART GC 主要负责 Java Heap 对象回收,不能直接解决应用进程中的 Native Heap、Bitmap native allocation、GL/Graphics buffer、dma-buf 等内存压力。kswapd 是后台异步回收线程,但当系统 zone 水位不足、空闲页不足、高阶页不足、CMA/碎片或 memcg 压力存在,且 kswapd 回收无法及时满足当前分配时,申请内存的应用线程会同步进入 direct reclaim。

mm_vmscan_direct_reclaim 属于内核内存管理路径,不是应用业务代码;但它可以运行在应用线程上下文中,例如 trace 中的 异步线程任务。当 应用的线程任务 进入该路径后,该线程会暂停原本的加载任务,转而在内核态执行内存扫描和回收。它不像 Java GC 那样全局 Stop-The-World,但对进入 direct reclaim 的线程来说是同步阻塞的。如果主线程正在等待该线程结果,就会表现为主线程长时间 Sleeping,并最终由该线程唤醒。

一句话总结

mm_vmscan_direct_reclaim 是内核为了满足当前内存分配,让申请内存的线程同步参与回收;它不是 Java GC,也不是上层应用进程的代码,但会发生在应用进程的线程上下文中。发生时不会像 GC 一样暂停整个进程,但会阻塞进入该路径的线程;如果主线程等待这个线程,就会造成 UI 卡顿。

一个有趣的AI大模型知识网站

相关推荐
plainGeekDev1 小时前
ContentProvider → Room + Repository
android·java·kotlin
plainGeekDev2 小时前
SQLite 手动升级 → Room Migration
android·java·kotlin
MemoriKu2 小时前
Flutter 相册 APP 视频模态稳定化实战:从视频抽帧、Embedding 元数据到 Android 真机启动修复
android·开发语言·前端·flutter·架构·音视频·embedding
Che2n3JigW2 小时前
Now in Android:它不是最佳实践,而是大型 Android 工程实践的展示
android·architecture·now in android
故渊at2 小时前
第三板块:Android 图形渲染与窗口体系 | 第十三篇:SurfaceFlinger 与 VSYNC 信号机制
android·图形渲染·surfaceflinger·帧率·窗口体系
Che2n3JigW2 小时前
Now in Android Feature 模块分析:一个功能是如何被组织起来的?
android·udf·architecture·now in android·modularization·feature module
Che2n3JigW2 小时前
Now in Android 项目结构分析:这个 App 是如何搭建起来的?
android·architecture·now in android·modularization·structure
恋猫de小郭2 小时前
flutter_agent_lens 用 MCP 服务,将 Flutter DevTools 暴露给 AI
android·前端·flutter
AI玫瑰助手3 小时前
Python函数:内置函数(len/max/min/sorted等)详解
android·开发语言·python