Android 的内存管理横跨内核、native 守护进程和 Java 框架三层。从虚拟内存的基础概念(VMA、页表、mmap),到 lmkd 守护进程的 PSI(Pressure Stall Information)监控,再到 AMS 的 OOM 调整算法——理解这一整套机制对性能优化和稳定性问题排查至关重要。
一、虚拟内存基础
1.1 VMA(Virtual Memory Area)
每个 Linux 进程都有一个独立的虚拟地址空间,由 vm_area_struct(VMA)来描述每个映射区域:
struct vm_area_struct { unsigned long vm_start; unsigned long vm_end; struct mm_struct *vm_mm; pgprot_t vm_page_prot; unsigned long vm_flags; const struct vm_operations_struct *vm_ops; };
|
在 /proc/<pid>/maps 文件中可以看到进程的所有 VMA 映射,例如:
12c00000-12c40000 rw-p 00000000 00:00 0 [anon:dalvik-main space] 7000000000-7000001000 r--p 00000000 103:09 1234 /system/lib64/libc.so
|
1.2 mmap 系统调用
mmap 是内存管理中最关键的系统调用之一,它将文件或匿名内存映射到进程的地址空间:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
|
Android 中的关键应用:
- Binder mmap:
ProcessState 初始化时通过 mmap 映射内核缓冲区(默认 1MB - 8*4096)
- ashmem mmap:匿名共享内存,用于 SurfaceFlinger 的 GraphicBuffer
- ART 的 dex mmap:将 DEX/VDEX 文件 mmap 到进程空间用于类加载
- 堆分配:通过
sbrk/mmap 扩展进程堆(Dalvik/ART heap)
1.2.1 mmap 的内核实现简析
unsigned long do_mmap(struct file *file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long pgoff, unsigned long *populate) { struct mm_struct *mm = current->mm;
addr = get_unmapped_area(file, addr, len, pgoff, flags);
struct vm_area_struct *vma = vm_area_alloc(mm); vma->vm_start = addr; vma->vm_end = addr + len; vma->vm_flags = calc_vm_prot_bits(prot, flags) | ...; vma->vm_page_prot = vm_get_page_prot(vma->vm_flags);
if (file) { vma->vm_file = get_file(file); vma->vm_ops = file->f_op->mmap; }
vma_link(mm, vma, prev, rb_link, rb_parent);
return addr; }
|
1.3 页表与 MMU
虚拟地址到物理地址的转换由 MMU(Memory Management Unit)通过页表完成:
虚拟地址 (Virtual Address) │ ▼ ┌─────────┬─────────┬──────────┐ │ PGD idx │ PUD idx │ PMD idx │ PTE idx │ offset │ │ (9bit) │ (9bit) │ (9bit) │ (9bit) │ (12bit)│ └────┬────┴────┬────┴────┬─────┴────┬────┴───┬────┘ │ │ │ │ │ ▼ ▼ ▼ ▼ │ PGD ──► PUD ──► PMD ──► PTE ──► Physical Page Frame │ ▼ Physical Memory
|
在 ARM64 平台上,通常使用 4 级页表(PGD -> PUD -> PMD -> PTE),每级 9 位索引(支持 48 位虚拟地址,即 256TB)。页大小通常是 4KB(也可以使用 16KB 或 64KB huge page)。
TLB(Translation Lookaside Buffer)缓存了最近使用的虚拟-物理映射,是 MMU 性能的关键。TLB miss 会导致遍历页表(walk page tables),代价昂贵。
1.4 内存度量指标
| 指标 |
含义 |
查询方式 |
计算复杂度 |
| VSS |
虚拟内存总量(含未分配的保留地址) |
/proc/<pid>/status 中的 VmSize |
O(1) |
| RSS |
实际占用的物理内存(含共享库) |
VmRSS |
O(1) |
| PSS |
按比例分摊共享内存后的物理内存 |
VmPSS(需要遍历页表,更耗时) |
O(n) |
| USS |
进程独占的物理内存 |
PSS - 共享内存分摊部分 |
O(n) |
计算复杂度:VSS < RSS < PSS < USS。lmkd 使用 RSS 做快速判断,AMS 的 updateOomAdjLocked 使用 PSS 做更精确的评估。
1.4.1 PSS 的计算方式
static void smaps_pte_entry(pte_t *pte, unsigned long addr, struct mm_walk *walk) { struct mem_size_stats *mss = walk->private; struct page *page;
if (pte_present(*pte)) { page = pte_page(*pte); mss->resident += PAGE_SIZE; mss->pss += PAGE_SIZE / page_count(page); if (page_mapcount(page) <= 1) { mss->private_clean += PAGE_SIZE; } } }
|
举例:如果 libc.so 的一个 4KB 页被 10 个进程共享,那么:
- 每个进程的 RSS +4KB
- 每个进程的 PSS +0.4KB (4KB / 10)
- 每个进程的 USS +0KB(因为不是独占)
二、Android 的 Low Memory Killer 演进
2.1 内核 LMK(Android 9 及之前)
早期 Android 使用内核 LMK 驱动(drivers/staging/android/lowmemorykiller.c)。它基于空闲内存阈值直接杀进程,缺点是阈值调整粗粒度、缺乏上下文感知。
内核 LMK 的工作方式:
static int lowmem_shrink(struct shrinker *s, struct shrink_control *sc) { for (i = 0; i < array_size; i++) { if (lowmem_minfree[i] > other_free || ...) { min_score_adj = lowmem_adj[i]; break; } } selected_oom_score_adj = ...; send_sig(SIGKILL, selected_task, 0); }
|
内核 LMK 的核心缺陷:
- 只关心空闲内存:不感知进程是否对用户可见,可能误杀前台应用。
- 阈值静态配置:
/sys/module/lowmemorykiller/parameters/minfree 中的阈值需要设备制造商根据 RAM 大小预设,无法动态调整。
- 无上下文感知:不知道哪个进程是后台音乐播放,哪个是纯粹缓存。
2.2 用户空间 lmkd(Android 10+)
Android 10 开始使用用户空间守护进程 lmkd。它通过 PSI(Pressure Stall Information)监控系统内存压力,同时结合 AMS 传来的 OOM 分数做决策:
lmkd 守护进程(native,system/core/lmkd/) ├── 读取 /proc/pressure/memory 监控内存压力 ├── 通过 /proc/<pid>/oom_score_adj 读取进程状态 ├── 使用 cgroup memory 限制或直接 kill 进程 └── 通过 lmkd socket 与 AMS 通信(AMS 主动请求杀进程)
|
AOSP 路径:system/core/lmkd/lmkd.cpp
2.2.1 lmkd 的主要回收策略
static void mp_event_common(int data, uint32_t events) { bool critical = (events & PSI_CRITICAL) != 0;
if (critical) { find_and_kill_processes(CACHED_APP_MAX_ADJ, ...); } else { find_and_kill_processes(PERCEPTIBLE_APP_ADJ, ...); } }
static void vmpressure_handler(int level) { switch (level) { case VMPRESSURE_LEVEL_LOW: break; case VMPRESSURE_LEVEL_MEDIUM: break; case VMPRESSURE_LEVEL_CRITICAL: break; } }
|
2.2.2 lmkd 和 AMS 的协作
AMS 通过 lmkd socket 向 lmkd 发送进程的 OOM 分数变更:
private void applyOomAdjLocked(ProcessRecord app, ...) { int oomAdj = computeOomAdjLocked(app, ...);
FileUtils.stringToFile("/proc/" + app.pid + "/oom_score_adj", Integer.toString(oomAdj));
if (mAppProfiler.mLmkdConnection != null) { LmkdStats.sendOomScoreAdjUpdate(mAppProfiler.mLmkdConnection, app.pid, app.uid, oomAdj); } }
|
lmkd 也可以通过 socket 主动请求 AMS 降低某个进程的 OOM 分数(使其优先被杀):
lmkd ----[LMK_PROCPRIO]----> AMS "这个进程的 OOM 分数太低,请调整到 800"
AMS ----[LMK_PROCREMOVE]--> lmkd "这个进程已经不存在了,请移除"
|
PSI 是 Linux 4.20+ 引入的内核特性,通过 /proc/pressure/ 暴露了系统因内存、IO、CPU 不足而导致的任务阻塞时间:
# cat /proc/pressure/memory some avg10=0.00 avg60=0.00 avg300=0.00 total=0 full avg10=0.00 avg60=0.00 avg300=0.00 total=0
|
some:至少有一个任务被阻塞
full:所有任务都被阻塞(整个系统完全停滞)
avg10/avg60/avg300:过去 10 秒/60 秒/300 秒的平均阻塞百分比
lmkd 通过 epoll 监控 /proc/pressure/memory,当内存压力超过阈值时触发回收动作。
bool PSIMonitor::open() { mFd = TEMP_FAILURE_RETRY(::open( "/proc/pressure/memory", O_RDONLY | O_CLOEXEC)); return mFd >= 0; }
void PSIMonitor::poll() { epoll_event events[MAX_EPOLL_EVENTS]; int nevents = epoll_wait(mEpollFd, events, MAX_EPOLL_EVENTS, timeout_ms);
for (int i = 0; i < nevents; i++) { processPsiEvent(events[i]); } }
|
2.3.1 PSI 阈值配置
lmkd 的 PSI 阈值是可配置的:
# /vendor/etc/lmkd.conf (vendor 配置) # PSI 阈值(百分比 * 100),达到后触发回收 ro.lmk.low=100 # 轻微压力 (1%) ro.lmk.medium=800 # 中等压力 (8%) ro.lmk.critical=1500 # 严重压力 (15%)
|
PSI 比旧的内核 LMK 的优势:
- 精确感知:PSI 直接度量任务的阻塞时间,而非间接的空闲内存量。
- 分级响应:some/full 提供不同的压力级别,lmkd 可以渐进式响应。
- 提前预警:在系统完全 OOM 之前就采取行动(full 压力发生之前)。
三、oom_score_adj 体系
3.1 核心值定义
AMS 通过 ProcessList 定义了完整的 OOM 调整体系:
static final int NATIVE_ADJ = -1000;
static final int UNKNOWN_ADJ = 1001; static final int CACHED_APP_MAX_ADJ = 906; static final int CACHED_APP_MIN_ADJ = 900; static final int SERVICE_B_ADJ = 800; static final int PREVIOUS_APP_ADJ = 700; static final int HOME_APP_ADJ = 600; static final int SERVICE_ADJ = 500; static final int HEAVY_WEIGHT_APP_ADJ = 400; static final int BACKUP_APP_ADJ = 300; static final int PERCEPTIBLE_APP_ADJ = 200; static final int VISIBLE_APP_ADJ = 100; static final int FOREGROUND_APP_ADJ = 0; static final int PERSISTENT_SERVICE_ADJ = -700; static final int PERSISTENT_PROC_ADJ = -800; static final int SYSTEM_ADJ = -900; static final int NATIVE_ADJ = -1000;
|
3.2 AMS 中 OOM 调整的计算
void updateOomAdjLocked() { for (int i = mLruProcesses.size() - 1; i >= 0; i--) { ProcessRecord app = mLruProcesses.get(i);
if (app.isKilledByAm() || app.thread == null) { app.curAdj = UNKNOWN_ADJ; } else if (app == mPreviousProcess && app.hasActivities()) { app.curAdj = PREVIOUS_APP_ADJ; } else if (app.mState.hasForegroundActivities()) { app.curAdj = FOREGROUND_APP_ADJ; } else if (app.mState.hasVisibleActivities()) { app.curAdj = VISIBLE_APP_ADJ; } else if (app.mState.hasForegroundServices()) { app.curAdj = PERCEPTIBLE_APP_ADJ; } else if (app.mState.hasStartedServices()) { app.curAdj = SERVICE_ADJ; } else if (app.mState.hasClientActivities()) { app.curAdj = VISIBLE_APP_ADJ; } else { app.curAdj = CACHED_APP_MIN_ADJ; }
applyOomAdjLocked(app, true, now, nowElapsed); } }
|
3.3 OOM ADJ 的写入
private boolean applyOomAdjLocked(ProcessRecord app, boolean doingAll, long now, long nowElapsed) { FileUtils.stringToFile("/proc/" + app.pid + "/oom_score_adj", Integer.toString(app.curAdj));
int oomScore = (int)((app.curAdj * OOM_SCORE_ADJ_MAX) / 1000); FileUtils.stringToFile("/proc/" + app.pid + "/oom_score", Integer.toString(oomScore)); }
|
3.3.1 oom_score_adj 的内核实现
static ssize_t oom_score_adj_write(struct file *file, ...) { struct task_struct *task; int oom_score_adj;
oom_score_adj = ...; if (oom_score_adj < -1000 || oom_score_adj > 1000) return -EINVAL;
task->signal->oom_score_adj = oom_score_adj;
}
unsigned long oom_badness(struct task_struct *p, ...) { unsigned long points;
points = get_mm_rss(p->mm) + ...;
adj = (long)p->signal->oom_score_adj; points = points * (1000 + adj) / 1000;
return points > 0 ? points : 1; }
|
四、cgroup 内存控制
Android 使用 Linux cgroup 对进程进行分组管理。lmkd 可以通过 cgroup memory 子系统限制进程或进程组的内存使用:
cat /proc/<pid>/cgroup
echo <limit_in_bytes> > /sys/fs/cgroup/memory/<group>/memory.limit_in_bytes
|
当进程超过 memory.limit_in_bytes 时,内核会触发 cgroup OOM,lmkd 可以设定更细粒度的内存限制策略。
Android 的 cgroup 层次结构:
/sys/fs/cgroup/ ├── memory/ │ ├── foreground/ # 前台应用 │ │ ├── uid_1000/ │ │ └── uid_1001/ │ ├── background/ # 后台应用 │ └── system/ # 系统服务 ├── cpu/ │ ├── foreground/ │ └── background/ ├── cpuset/ │ ├── foreground/ │ ├── background/ │ └── top-app/ # 前台顶级应用 └── freezer/ # Android 11+ 的进程冻结 ├── frozen/ └── unfrozen/
|
4.1 cgroup freezer(Android 11+)
Android 11 引入了 cgroup freezer 来替代之前的 SIGSTOP/CON 信号:
static void freezeProcess(int pid, int uid) { }
static void unfreezeProcess(int pid, int uid) { }
|
cgroup freezer 的优势:
- 比信号方式更可靠(SIGSTOP 可以被捕获或忽略)
- 可冻结线程组(整个进程的所有线程)
- 内核级别的保证(冻结的进程无法分配 CPU 时间)
五、进程内存状态(procState)
除了 oom_adj,AMS 还维护进程状态(process state),用于更细粒度的管理和统计:
public static final int PROCESS_STATE_NONEXISTENT = -1; public static final int PROCESS_STATE_TOP = 2; public static final int PROCESS_STATE_BOUND_TOP = 3; public static final int PROCESS_STATE_FOREGROUND_SERVICE = 4; public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE = 5; public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 6; public static final int PROCESS_STATE_IMPORTANT_BACKGROUND = 7; public static final int PROCESS_STATE_TRANSIENT_BACKGROUND = 8; public static final int PROCESS_STATE_BACKUP = 9; public static final int PROCESS_STATE_HEAVY_WEIGHT = 10; public static final int PROCESS_STATE_SERVICE = 11; public static final int PROCESS_STATE_RECEIVER = 12; public static final int PROCESS_STATE_HOME = 13; public static final int PROCESS_STATE_LAST_ACTIVITY = 14; public static final int PROCESS_STATE_CACHED_ACTIVITY = 15; public static final int PROCESS_STATE_CACHED_ACTIVITY_CLIENT = 16; public static final int PROCESS_STATE_CACHED_RECENT = 17; public static final int PROCESS_STATE_CACHED_EMPTY = 18;
|
六、ART 的堆内存管理
6.1 ART 堆的分配策略
class Heap { ImageSpace* image_space_; M allocSpace* alloc_space_; LargeObjectSpace* los_; ZygoteSpace* zygote_space_; BumpPointerSpace* region_space_; };
|
ART 堆结构图:
┌─────────────────────────────────────┐ 高地址 │ Large Object Space │ 对象 >= 12KB │ (free list allocator) │ ├─────────────────────────────────────┤ │ Non-moving Space │ │ (dlmalloc / RosAlloc) │ ├─────────────────────────────────────┤ │ Zygote Space (共享) │ │ (从 Zygote 继承的预加载类) │ ├─────────────────────────────────────┤ │ Image Space (共享) │ │ (boot.art, boot-framework) │ └─────────────────────────────────────┘ 低地址
|
6.2 GC 策略演进
ART 的 GC 经历了多次重大改进:
- **Dalvik (Android 4.4 以前)**:Mark-Sweep,非压缩,stop-the-world。
- **ART (Android 5.0-5.1)**:Concurrent Mark-Sweep (CMS),部分并发,减少暂停时间。
- **ART (Android 6.0-7.x)**:引入 Compacting GC (Semi-Space) 用于前台应用,后台应用仍用 CMS。
- **ART (Android 8.0-9.x)**:Concurrent Copying (CC) GC,基于 Read Barrier,最小化暂停时间。
- **ART (Android 10+)**:Generational Concurrent Copying (GCC) GC,基于分代假设,进一步提升吞吐。
6.2.1 Concurrent Copying GC 原理
class ConcurrentCopying : public GarbageCollector { void RunPhases() { InitializePhase();
MarkingPhase();
PausePhase();
CopyPhase();
ReclaimPhase(); } };
|
Read Barrier(读屏障)是 CC GC 的核心技术。当线程读取一个对象引用时,如果该对象已经被复制到新区域,读屏障会自动将引用更新到新地址:
线程读取 obj.field │ ▼ obj 是否已移动? / \ 是 否 │ │ ▼ ▼ 返回新地址 返回原地址 (Read Barrier) (无屏障开销)
|
6.2.2 Generational CC GC (Android 10+)
基于分代假设(大多数对象很快死亡),将堆分为 Young Generation 和 Old Generation:
Young Generation (Region Space) ├── 新分配的对象 ├── Minor GC 频繁发生(快速回收短生命周期对象) └── 存活一定次数的对象晋升到 Old Generation
Old Generation ├── 长期存活的对象 ├── Major GC 偶尔发生(全面回收) └── 写屏障记录 Young->Old 引用
|
通过分代回收,可以减少扫描的对象数量,降低 GC 暂停时间。
七、核心面试题
Q1:Android 为什么要从内核 LMK 迁移到用户空间 lmkd?
内核 LMK 的决策完全基于空闲内存阈值,不感知进程的实际重要性。例如,一个正在播放音乐的 Service 和一个缓存进程在内核 LMK 看来可能具有相同的优先级。用户空间 lmkd 结合了 AMS 的知识(哪个进程在前台、哪个有可见 Activity),以及内核的 PSI 信息,做出更智能的决策。此外,用户空间 lmkd 可以通过 cgroup 实现更灵活的内存限制策略。
Q2:PSS 和 RSS 的区别是什么?Android 为什么偏好使用 PSS 来评估进程内存占用?
RSS 统计进程占用的全部物理内存页(包括与其他进程共享的库如 libc.so)。PSS 将共享内存页按共享进程数按比例分摊(如果 5 个进程共享 100KB,每个进程 PSS 只记 20KB)。Android 使用 PSS 的原因:它更准确地反映了”如果杀这个进程,能腾出多少物理内存”。但 PSS 计算开销大(需要遍历所有页表),因此 AMS 只在必要时触发 PSS 收集。
Q3:oom_score_adj 的范围是多少?FOREGROUND_APP_ADJ=0,CACHED_APP_MAX_ADJ=906,这些值为什么这么定义?
Linux 内核的 oom_score_adj 范围是 [-1000, 1000]。Android 的 adj 值与其一一对应:
- 负值(-1000 到 -1):系统关键进程,不考虑 kill(NATIVE_ADJ=-1000, SYSTEM_ADJ=-900, PERSISTENT_PROC_ADJ=-800)
- 0-99:前台/可见进程
- 100-199:可感知进程
- 200-899:有后台 Service 的进程
- 900-906:纯缓存进程,最先被 kill
这种分段使得 lmkd 可以用阈值区间来控制回收策略,比如”当内存压力达到 critical 时,先回收 CACHED_APP 层级的进程”。
Q4:mmap 和 brk 有什么区别?Android 的 ART 堆使用哪种方式分配内存?
brk/sbrk:通过调整进程的数据段末尾指针来分配/释放内存。只能分配连续的内存,不能释放中间的内存块(只能从末尾释放)。适用于小块内存的快速分配。
mmap:在进程地址空间的任意位置映射一块内存。可以独立释放(通过 munmap),更灵活但比 brk 慢(需要操作 VMA 和页表)。
ART 堆的分配策略:
- 小对象(< 12KB):使用 RosAlloc 或 dlmalloc(基于 brk 和 mmap 混合)。
- 大对象(>= 12KB):使用 mmap 分配(Large Object Space),按页大小对齐。
- Image Space / Zygote Space:使用 mmap 映射预编译的 .art 文件。
Q5:cgroup freezer 和 SIGSTOP 冻结进程有什么区别?
- SIGSTOP:发送信号给进程,进程被暂停。但进程可以捕获并处理信号,可能不完全停止。某些系统调用在收到信号后会自动重试。如果进程被 SIGSTOP 时正在持有锁,可能导致死锁。
- cgroup freezer:内核级别的冻结机制。通过将进程移入 frozen cgroup,内核调度器停止调度该进程的所有线程。进程无法感知到被冻结(无信号传递),不会造成锁持有问题。冻结和解冻都是原子操作,更可靠。
Android 11+ 使用 cgroup freezer 来替代 CachedAppOptimizer 中的 SIGSTOP 机制,提供了更可靠的进程冻结能力。
核心参考 AOSP 路径:
frameworks/base/services/core/java/com/android/server/am/ProcessList.java
frameworks/base/services/core/java/com/android/server/am/OomAdjuster.java
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
system/core/lmkd/lmkd.cpp
system/core/lmkd/libpsi/ — PSI 监控库
drivers/staging/android/lowmemorykiller.c — 旧内核 LMK
mm/mmap.c — mmap 内核实现
include/linux/mm_types.h — VMA 定义
art/runtime/gc/heap.cc — ART 堆管理
art/runtime/gc/collector/concurrent_copying.cc — CC GC 实现