大内存申请实测:自定义应用 1GB→2GB 分阶段实验

需求来源:big_mem_test.md | iPhone 12 Pro(A14 / 6GB / iOS 14.8 / Taurine 越狱 / 16KB 页)| 实验工具:phasedmem.c(本仓库 ios-probe/cli)| 2026-08-30
启动申请 1GB 耗时
259ms
空闲局(3.95 GB/s);满载局 392ms(2.61 GB/s),仅慢 52%
满载局 free 起点
36MB
五 App 满载极限局,2GB 申请零击杀全完成
供给主力(满载局)
压缩器
五 App 1GB+ 匿名页被同步压入,压缩比约 3:1
释放 2GB 耗时
143ms
munlock 95ms(10.3GB/s)+ munmap 48ms(41.8GB/s)
文件页回收硬地板
856MB
cleaned→投机→冷文件页→地板;触底后只压缩匿名(reactivated +11,652)

0. 实验设计

自定义应用 phasedmem 模拟相机的分阶段内存申请行为:

阶段动作模拟对象
STAGE1启动即申请 1GB = 512MB 匿名 zero-fill + 512MB mlock wiredIOSurface pageable 形态 + 驱动 wired 形态
STAGE24 步 × 256MB(128 匿名 + 128 wired),步间隔 5s → 2GB相机管线扩容
SETTLE静置 30 秒稳态观察
RELEASEmunlock 全部 wired → munmap 全部用户态释放效率

采样:0.5s 粒度整机 vm_stat 全字段 + appscan 进程级 footprint 轨迹;工具自带毫秒级自计时。

1. 场景一:清场后 free=2.83GB

1.1 时间线

t free wired anon comp 事件 0s 181,040 44,780 32,525 81,809 phasedmem 启动(1GB 申请中) 1s 118,941 77,587 61,855 81,806 STAGE1 完成: wired +512MB, anon +460MB, free -966MB 5s 102,568 85,741 70,072 81,802 grow1: wired +127MB 11s 85,563 93,924 78,848 81,302 grow2: wired +128MB 16s 69,150 102,117 86,952 81,294 grow3: wired +128MB 21s 52,830 110,309 95,165 81,283 STAGE2 完成(2GB): wired 共 +1,024MB ✓ 21~50s 52,827 110,311 95,168 81,283 静置 30s: 满载纹丝不动 51s 176,610 44,826 33,195 78,967 释放: wired -1,024MB, free +1,428MB(含静置期回填) 55s 181,626 44,835 25,122 86,190 进程退出: free 回到起点(+9MB 净差)

1.2 进程 footprint 轨迹

t= 0s fp=1025.6MB internal=1024.9MB compressed= 0.0MB t= 28s fp=2050.1MB internal=2048.9MB compressed= 0.0MB ← 全解压驻留 t= 54s fp= 1.1MB internal= 0.9MB compressed= 0.0MB ← 释放干净

1.3 全程账本(首样本 → 释放前)

字段起点终点Δ解读
Pages free181,040181,626+9MB净零——申请的都还回去了
wired down44,78044,835+01GB wired 全额摘除
compressor stored81,80986,190+68MB系统背景噪声(其它 App churn)

2. 场景二:五 App 满载,free=36MB 极限局

前置:拉起 抖音 / 淘宝 / 京东 / 网易云音乐 / bilibili 五 App 至满载(free 跌至 2,310 页 = 36MB)。

2.1 时间线

t free wired anon fb comp 事件 0s 3,047 52,366 151,333 110,454 102,363 phasedmem 启动(五App满载中) 1s 2,195 78,157 125,797 111,241 101,073 STAGE1: wired +402MB ← anon -412MB(五App被压) 6s 2,991 87,331 126,502 100,492 107,868 grow1: comp +58MB 内容继续供货 10s 2,789 95,523 120,479 98,561 121,963 grow2: comp 已 +170MB 16s 2,647 103,815 102,357 99,068 148,213 grow3: comp +370MB 21s 3,493 110,928 91,774 97,180 167,893 STAGE2 完成(2GB): comp +65,530页(+1,048MB内容) 21~50s 2,833 112,311 90,979 97,478 167,069 静置: 满载稳定, comp 不再涨 51s 91,153 46,479 68,672 97,479 124,170 释放: wired -1,029MB, free +1,401MB 55s 90,842 45,459 69,660 97,754 124,143 进程退出

2.2 进程 footprint 轨迹(关键差异!)

t= 0s fp=1025.6MB internal= 959.9MB compressed= 65.0MB t= 10s fp=1281.7MB internal=1106.6MB compressed=174.2MB t= 19s fp=1793.9MB internal=1280.7MB compressed=512.2MB t= 29s fp=2050.1MB internal=1377.9MB compressed=671.0MB ← 33%已被压! 与场景一(全解压)截然不同 t= 54s fp= 1.0MB internal= 0.2MB compressed= 0.7MB
核心发现:满载局里 phasedmem 自己的匿名页也有 671MB(33%)被压进压缩器——系统判定"新申请的冷匿名页"同样可压。wired 部分(1GB)则全程豁免(wired 计数精确 +1,024MB)。

2.3 供给链分析(free 仅 36MB,2GB 从哪来?)

供给来源数量机制
五 App 匿名页压缩−1,277MBvm_pageout 同步压缩供货(场景期间 comp stored +1,048MB 内容,物理占用仅 +340MB → 压缩比 3.1:1)
file-backed 摘除−199MB代码页/缓存页直接丢弃(可从磁盘回读)
free 池消耗−21MB几乎没动——本来就只有 36MB
击杀进程0 个五 App 全部存活,纯页级腾挪

2.4 五 App 终态(实验结束后)

Appfootprint内驻压缩态压缩占比
抖音 Aweme533.8MB1.3MB503.5MB94%
淘宝315.4MB0.8MB286.3MB91%
京东130.8MB2.3MB128.1MB98%
网易云音乐289.3MB157.8MB137.6MB48%
bilibili(番剧版)233.4MB151.5MB81.1MB35%

注:网易云/bili 的内驻偏高,因实验结束后部分页已被访问解压回驻。

3. 申请与回收效率(需求 §内存分析 2/3)

操作场景一(free 2.83GB)场景二(free 36MB)退化幅度
STAGE1 1GB(512a+512w)259ms(3.95GB/s)392ms(2.61GB/s)×1.52
— touch 匿名 512MB137ms(3738MB/s)108ms(4732MB/s)更快*
— touch+wired 512MB231ms(wire 6491MB/s)167ms(wire 7962MB/s)更快*
grow 每步 256MB61~76ms(3.3~4.1GB/s)58~95ms(复测)≈持平
STAGE2 全程(4×256MB)20.3s(含 5s 间隔)20.4s(含 5s 间隔)持平
释放 2048MB144ms143ms完全一致
— munlock 1024MB95ms(10.3GB/s)95ms(10.3GB/s)
— munmap 2048MB49ms(41.8GB/s)48ms(41.8GB/s)

* 场景二 touch 更快的原因:五 App 的页预先已在压缩器中"热身",zero-fill 直接消耗刚释放的 free 页,无需唤醒 pageout 扫描器。STAGE1 总时长变慢在于首秒同步压缩五 App 供货的等待。

回收效率的两层含义:① 用户态释放(munlock+munmap)143ms 无场景差异——纯地址空间操作+wired 摘链;② 系统级回收(场景二供给)是同步的:每步 wired +128MB 都伴随五 App 被压,压缩吞吐 ≈ 1GB/20s ≈ 50MB/s 净(WKdm 算法,§6.3 书中实测 240MB/s 级),不阻塞申请路径(mlock 等待 <100ms/步)。

4. 三个最终问题的回答

Q1:iOS 启动自定义应用会申请多少内存?系统需要供给多少?分别是哪些内存?

应用侧:启动即 1GB(512MB 匿名 zero-fill surface 模拟 + 512MB wired 驱动模拟),增长到 2GB(各 1GB)。进程 footprint 精确匹配(1025.6MB → 2050.1MB,与申请量差 <0.1%)。

系统侧供给量:场景一全从 free 池划扣(free 2.83GB 充足,申请后仍剩 825MB);场景二 free 仅 36MB,系统从"压缩五 App 匿名页(−1,277MB)+ 摘 file-backed(−199MB)"腾出 1.5GB+ 供货,压缩器物理占用净增仅 +340MB(内容 1,048MB,压缩比 3.1:1)。内存类型上:匿名页走 zero-fill 分配(计 zero_fill 计数器),wired 走 mlock→vm_page_wire(不可回收、全记账、释放前纹丝不动)。

Q2:申请耗时多长?效率多快?free 不足时如何回收供给?

1GB 启动申请 259ms(空闲)/ 392ms(满载),综合 2.6~4.0GB/s;分解后 touch 匿名 3.7~4.7GB/s、wire 6.5~8.0GB/s。free 不足时的供给机制:同步压缩——mlock 触发的缺页路径上,vm_pageout 直接压缩五 App 的冷匿名页(无需先写盘、无需杀进程),每步 128MB wired 供给等待 <100ms。整个 2GB 满载过程 零击杀、五 App 全存活

Q3:各统计值变化与合理性

统计值场景一变化场景二变化合理性判定
Pages free181k→181k(净零)3k→91k(+1.4GB)✓ 一:释放即回吐;二:释放后五 App 未完全回驻,页暂存 free
wired down+1,024MB 后归零+1,024MB 后归零✓ mlock 全额记账、munlock 全额摘除,两场景分毫不差
Anonymous−116MB(净)−1,277MB✓ 二:五 App 页被压后从 anon 转入 compressor 记账
compressor stored+68MB(噪声)+1,048MB 内容/+340MB 物理✓ 压缩比 3.1:1(WKdm 典型值)
File-backed持平−199MB✓ 系统摘冷代码页供货(可磁盘回读,最便宜来源)
进程 footprint峰值 2050.1MB 全解压峰值 2050.1MB(33% 压缩态)✓ 同为 2GB 但驻留形态不同——footprint 含压缩态

5. 文件页回收顺序分析

当 free 不足时,系统回收文件页的顺序是什么?本节用源码(xnu-7195.141.2)+ 两个专门实验回答:文件页由哪些成分组成、每种成分的回收优先级、以及地板保护在哪里生效。

5.1 文件页的组成

成分性质回收成本本机实测占比
投机页(speculative)预读簇读入但从未被访问(clustered read-ahead 超出请求范围的部分)零——直接丢弃,无 I/O基线 25,474 页(398MB)= fb 的 21%(压力后重建池约 3,000 页)
冷文件页(inactive external)代码段(dylib 文本,来自 dyld 共享缓存)、mmap 的资源文件、字体文件、资产目录——曾被访问后老化低——clean 直接丢弃;被再次访问时从磁盘 pagein 回来(约 200μs/页)fb − spec ≈ 95,259 页(1.49GB)= 79%
脏文件页(dirty)mmap 可写映射被写过的页高——必须先写回(pageout)变 clean 进 cleaned 队列才能回收本实验工作负载下接近 0(两场景 Pageouts 仅 +73 页 / +1MB)
活跃文件页(active)近期访问过的代码/资源(热路径)最高——需先老化降级到 inactive 才可回收计入 active,不单列
注意 vm_stat 口径:"Pages free" 包含 speculative(host.c:815 free_count = vm_page_free_count + vm_page_speculative_count);"File-backed" = speculative + 活跃文件页 + 冷文件页 + cleaned。所以 speculative 是文件页的子集,也是"可用内存"的隐藏分量。

5.2 源码级回收顺序链(vm_pageout.c victim 选择)

pageout 扫描器 vm_pageout_scan 每轮从多个队列按优先级挑选牺牲页(xnu-7195.141.2 osfmk/vm/vm_pageout.c):

优先级队列源码锚点语义
① 最优先cleaned Qvm_pageout.c:2359-2365脏页已经写回变 clean 的页——I/O 成本已付,直接摘除("Try for a clean-queue inactive page... Pick them up now that they are clean")
② 次优先speculative aged Qvm_pageout.c:2367-2378预读但从未被触摸、且已度过 10×500ms=5 秒老化保护期的投机页——零 I/O 直接丢
background Qvm_pageout.c:2380-2419后台模式(darkwake)下的 external 页(CONFIG_BACKGROUND_QUEUE)
inactive external Qvm_pageout.c:2448+普通冷文件页——但受 filecache_min 地板保护:低于地板时改 reactivate 放回(:2475-2490,计数器 vm_pageout_filecache_min_reactivated
⑤ 兜底inactive internal Qvm_pageout.c:2505+匿名页——送压缩器(不是丢弃)

三个调节器(同一文件):

5.3 实测验证一:瞬时冲击的供给分解

1.5GB 匿名压力在 0.5 秒内施加(工具 anonpress.c,ios-probe/cli),单采样窗口内的供给来源分解:

供给 1,536MB = 真free 池 −259MB (19,498 → 2,913 页) 投机池 −398MB (25,474 → 239 页, 100% 排空! ①②级牺牲品) 冷文件页 −255MB (inactive external 摘除) 压缩其他进程匿名 +624MB (compressor 同步供货, 异步完成)

投机池在单个 0.5s 窗口内从 25,474 页被排空到 239 页(−398MB)——这正是源码里 speculative aged Q 排在 inactive external 之前的实测映照:零成本牺牲品先死。同时 fb 从 120,733 → 79,161(−650MB),其中 398MB 来自投机池、255MB 来自冷文件页。

5.4 实测验证二:阶梯加压下的 fb 硬地板

逐级加压 6 × 512MB(每级稳 7 秒)观察 fb 的阶梯响应——用于区分"fb 不降"是地板保护还是"不再需要回收":

累计压力freespecfile-backedfb 较上级压缩次数(累计Δ)reactivated(Δ)供给主力
512MB92,6333,15682,627+2MB010free 池
1,024MB59,6163,22182,715+1MB054free 池
1,536MB26,6063,26182,795+1MB060free 池
2,048MB3,08062076,659−95MB3,4641,090free 枯竭 → 投机池排空(3,261→620)
2,560MB2,81062854,948−339MB17,03811,652冷文件页大额摘除 + 压缩器启动
3,072MB3,01059854,821−1MB50,01011,741fb 钉死地板,全部靠压缩器

三个决定性信号同时出现:

5.5 与大内存实验的互相印证

实验file-backed 变化解释(用本节顺序链)
场景一(free 2.83GB 充足)持平(+71MB 噪声)free 池全额支付,回收链根本未被唤醒——供给顺序第 0 级
场景二(五 App 满载 free 36MB)−199MB首波冲击的冷文件页摘除(顺序链 ④ 级)+ 投机池排空;之后 fb 钉在 97k 页不再降——同为 filecache_min 地板
本节瞬时冲击(1.5GB/0.5s)−650MB投机池全额(−398MB)+ 冷文件页(−255MB)单窗内依次支付
本节阶梯加压(3GB)−432MB自由段(1.5GB 内)不动 → 投机池 → 冷文件页 → 856MB 硬地板后全靠压缩器
一句话总结:文件页回收顺序 = cleaned(已写回的脏页)→ speculative(零成本投机页)→ 冷文件页(inactive external)→ 856MB 地板(filecache_min 保护,再往下只压缩匿名页不碰文件页)。脏文件页走特殊路径:先写回进 cleaned 队列插队到最优先。活跃文件页需先老化降级。整条链的设计逻辑:按"回收成本"升序支付——零 I/O 的先死,需要 I/O 的最后死,需要写 I/O 的要预付

6. 实验过程记录(诚实数据)