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大模型知识网站

相关推荐
千里马学框架21 小时前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台21 小时前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone21 小时前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone1 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen1 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone1 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui