想象你走进一座巨大的图书馆。地下仓库里存着一套 200 卷的百科全书——这正是那个动辄几十 GB 的 GGUF 大模型文件,沉得一个人根本搬不动。管理员递给你一张借阅卡,说:“拿着它,去阅读室吧。”你走进阅读室,眼前竟出现了整排书架,200 卷书仿佛触手可及。可蹊跷的是,当你伸手翻到第 37 卷某一页时,管理员才匆匆跑进仓库,把那一页捧到你面前。你翻到哪一页,他就送哪一页;没翻到的书页,始终安安静静躺在仓库里。这就是 mmap(内存映射)的魔法:让巨大的文件“看起来”全在眼前,却只在真正用到时才从磁盘搬进内存。
掌握 mmap,你就理解了为什么 16GB 内存的笔记本能跑 70 亿参数的模型。这是 AI 部署工程师解决“内存装不下大模型”的第一把钥匙,也是读懂 llama.cpp / Ollama 启动原理的必修课。
可视化演示
演示GGUF场景下mmap内存映射机制:映射建立展示文件到虚拟地址空间的映射;按需加载展示页面经缺页调度从磁盘载入物理内存;内存对比展示mmap按需加载相对全量read的内存与启动优势。
本节配有交互动效(进入视口后自动加载,约 3KB)。
mmap映射建立 文件→虚拟地址空间 mmap() 映射建立仅需毫秒级
文件映射基础
借阅卡的第一重魔法,是让 200 卷百科全书“投影”进阅读室。你看到的整排书架,其实是管理员用一面虚拟的镜子映出来的——书还在仓库,但每一卷的书脊都清清楚楚挂在眼前。这张“投影”,就是 mmap 建立的内存映射。它做的只是一次登记:告诉操作系统“把文件第 0 到第 N 字节,和我进程里地址 X 到 X+N 这一段对应起来”,登记完立刻返回,一本书都不用搬。
从技术上讲,mmap 把文件的一段区域映射进进程的虚拟地址空间。虚拟地址空间是每个进程独享的“阅读室平面图”,64 位系统下大得惊人(约 1.8×10¹⁹ 字节),足以装下任何模型文件。真正把“平面图上的坐标”和“仓库里的书架”连起来的,是页表——它记录每个虚拟页对应哪个文件页。映射方式上,MAP_SHARED 让多个进程共享同一份物理页,MAP_PRIVATE 则写时复制;GGUF 推理通常用只读私有映射,配合内核页缓存实现物理共享。
有了这张借阅卡,后续所有操作都在这面“投影”上完成。模型文件没有被复制进内存,它只是被“登记”在了你的阅读室里——这正是后面一切好处的起点。
- base(映射基址)mmap 返回的起始虚拟地址,相当于阅读室书架的起点,一切映射内容都从这个地址开始摆放。
- offset(文件内偏移)目标数据距离文件开头的字节数,就像目录卡上标注的“第 37 卷第 12 页”。
- vaddr(虚拟地址)基址加偏移得到的进程虚拟地址,CPU 访问它,就像在阅读室伸手去够那本书。
按需加载机制
借阅卡的第二重魔法,是“翻到哪页送哪页”。你站在书架前,伸手翻到第 37 卷某一页,管理员才跑进仓库把那一页捧来;你翻到第 88 卷时,他又去取第 88 卷。没翻过的卷,绝不会被搬到阅读室。这个过程在操作系统里叫按需加载,其核心引擎是缺页中断。
当程序访问一个已映射但尚未加载的虚拟页时,CPU 发现页表里没有对应的物理页,就会触发缺页中断,像按响书桌上的服务铃。内核接到铃后,从磁盘读入这一页(通常 4KB),放进页缓存,更新页表,然后让程序继续执行。放到页缓存里的数据会被保留,下次再读同一页就是“秒回”,不必再跑仓库。推理过程中,模型权重被顺序扫描时,页缓存命中率极高,访问速度接近纯内存。
所以,一个 4GB 的模型文件,若只访问了其中 200MB 的权重,物理内存就只多占用约 200MB。管理员很勤快,但也绝不超前服务——这就是大模型能跑在小内存机器上的根本原因。
- 已访问字节数程序到目前为止真正读过的数据总量,对应你翻过的书页总数。
- 页大小操作系统搬运数据的最小单位,就像管理员一次只能捧一页纸,x86 上默认 4096 字节。
- 向上取整即使只读了半页,也要按整页占用计算,因为版权“一整页”是一次性送到桌上的。
GGUF布局结构
要让管理员“指哪打哪”,图书馆必须有一套精确的目录系统。GGUF 文件本身就是一座设计精良的图书馆:文件头是总目录,张量元数据是篇目索引卡,权重数据区是真正的正文。文件头固定 24 字节,依次写着魔数 GGUF、版本号、张量数量、键值对数量,就像图书馆门口挂的“本馆共 200 卷,索引编号从 1 开始”。
紧随其后的是元数据区:先是若干 KV 键值对(记录上下文长度、词表大小等超参数),再是每个张量的信息表。每个张量条目记录四样东西——名字(如 token_embed.weight)、维度(形状)、数据类型(float32 还是 q4_0 量化)、以及最关键的数据偏移量。这个偏移量是张量权重在文件中的绝对字节位置,相当于索引卡上写明的“第 37 卷,第 120 页,第 3 段”。数据区则按对齐规则排列着所有原始权重字节。
这套目录之所以重要,是因为它让“按需加载”有了导航。推理引擎只要读出文件头和张量信息表(区区几 KB),就掌握了整本书的索引,接下来想读哪个权重,直接按偏移去取,不必翻遍全书。
- 头部长度固定 24 字节的定长头,包含魔数、版本、张量数、KV 数。
- 累加符号 Σ把前面 n 段元信息(KV 键值对和其他张量的信息表)的长度逐一相加。
- 张量偏移算出的结果就是目标权重在文件中的起始字节位置,供 mmap 直接定位。
零拷贝权重访问
管理员把第 37 卷那一页摊开放在你面前,你直接在这页纸上圈圈点点做笔记,不需要把内容誊抄到自己的草稿本上——这就是零拷贝。传统的 read() 读文件,好比管理员先把内容抄一份给你,你再拿着抄件去计算,白白多花一次拷贝的时间和内存;而 mmap 让你直接读写操作系统页缓存里的原始页面。
在 Python 里,mm[offset:offset+size] 返回的切片,正是页缓存中权重字节的直接视图;在 C/C++ 推理引擎里,则把 映射基址 + 张量偏移 强转成张量指针,CPU 直接对着这段地址做矩阵乘法。整个过程没有一次应用层的数据复制,所以叫零拷贝。GGUF 推理时还会用只读映射(ACCESS_READ),既防止程序手滑写坏模型文件,又让多个进程能安全共享同一份页缓存。
零拷贝的意义,在于把“读取权重”的成本压缩到几乎为零:没有拷贝、没有用户态缓冲、没有额外内存。推理引擎拿到指针后直接计算,页缓存则默默承担了“把数据从磁盘搬到内存”的脏活累活。
- map_base(映射基址)mmap 返回的起始地址,所有文件内容在这之后的虚拟空间里“隐身”。
- tensor_offset(张量偏移)从 GGUF 元数据里读到的权重起始偏移,相当于索引卡上的页码。
- weight_ptr(权重指针)两者相加即得权重在虚拟内存中的地址,计算单元可像操作普通数组一样使用它。
推理性能优势
图书馆模式的好处是显而易见的。第一,快速启动:不用把 200 卷书搬进阅读室,你只需登记借阅卡,翻到目录就能开工。llama.cpp 加载一个 4GB 的 GGUF 模型,冷启动只需读几十 KB 的头部和张量信息表,几百毫秒内就能开始对话,而传统 read() 要把 4GB 全部读进内存才能开始。第二,低内存占用:RSS(常驻内存)只统计真正载入物理内存的页,没访问的权重不占地方。第三,多进程共享:API 服务用 fork 生出多个 worker 进程时,它们映射同一个 GGUF 文件,缺页后共享同一份页缓存,物理内存几乎只占一份,4 个 worker 不用 4 份权重。
更妙的是,页缓存具有“全局去重”能力:哪怕两个完全独立的进程各自 mmap 同一个文件,内核也只会为这些文件页保留一份物理副本。推理是典型的顺序访问模式,权重按层被逐层扫描,页缓存命中率非常高,实际吞吐接近纯内存计算。
图书馆越大,这种“按页借阅”的模式越划算。这也是为什么越大的模型,越要用 GGUF + mmap 来部署——它把“内存装不下”的难题,巧妙地转化成了“按需取用”的优雅方案。
- 载入页数 × 页大小实际进入物理内存的数据量,对应管理员真正送来的书页。
- 文件大小GGUF 文件的总体积,即整套百科全书的篇幅。
- 1 减去比值算出“没占用内存”的那部分比例,比值越接近 1,节省越多。
部署实战要点
把借阅卡真正用起来,还要考虑三件事:阅读室够不够大、仓库到阅读室的路好不好走、以及你要把书搬给谁看。第一,地址空间大小:64 位系统的虚拟地址空间接近无限,映射几十 GB 的模型毫无压力;但 32 位系统只有约 4GB 地址空间,映射大模型会直接失败,这也是 GGUF 部署要求 64 位环境的原因。第二,存储介质:缺页中断要从磁盘读数据,SSD 的随机读性能远超 HDD,把模型文件放 SSD 上能大幅减少首次访问的等待。第三,GPU 卸载:纯 CPU 推理直接读映射的权重即可;需要 GPU 时,推理引擎按层把 CPU 映射中的权重拷入显存,用完一层再换下一层,显存不够也能跑大模型。
Ollama 和 llama.cpp 默认启用 mmap,但它们也提供了开关(如 --no-mmap),让用户在“按需加载”与“启动即全量载入”之间权衡。量化 GGUF(如 q4_0)本身就把权重压缩到原来的四分之一,配合 mmap,一杯咖啡的功夫就能在笔记本上跑起 70 亿参数模型。此外,大模型文件通常只用只读映射,避免写操作触发页缓存回写带来的额外开销与风险。
部署的本质,是把合适的书放在合适的图书馆:地址空间够大、存储够快、按需搬运到 GPU。理解了这三条,你就能解释为什么同一台机器上,GGUF + mmap 能顺畅推理,而其他格式却频频 OOM。
- 地址位数CPU 寻址宽度,64 位系统理论地址空间为 2⁶⁴ ≈ 1.8×10¹⁹ 字节。
- 已用虚拟空间进程里已被代码、堆、栈和其他映射占用的地址范围。
- 可映射上限两者相减得到的剩余地址空间,决定你还能映射多大的模型文件。
风险与边界
借阅卡再方便,也敌不过物理定律。第一个风险是页抖动:如果同时访问的权重太多,超出了物理内存容量,管理员就不得不在“送新页”和“收旧页”之间疲于奔命,系统吞吐断崖式下降,比慢还慢。第二个风险是I/O 阻塞:缺页中断是同步操作,冷启动或首次访问时,线程会卡在磁盘读取上,表现为推理首 token 延迟很高。第三个风险是文件损坏:如果映射的文件被外部截断,而程序又访问到缺失区域,进程会收到 SIGBUS 信号直接崩溃——相当于你翻到一本书,发现那一页被撕掉了。
评测系统性能有一个经典指标:有效访问时间(EAT),它把“内存命中”和“缺页”两种情况的耗时按概率加权,清晰揭示抖动为何致命。
再好的借阅卡,也要尊重物理内存的底线。部署时估算好工作集大小、预留足够内存、把文件放在可靠的存储上,才能在享受 mmap 红利的同时,避开这些暗礁。
- p(缺页率)每次访问中触发缺页中断的概率,p 从 0 升到 0.1,性能就可能差一个数量级。
- t_m(内存访问时间)页缓存命中时的一次访问耗时,约几十纳秒,相当于从桌面拿书。
- t_d(缺页处理时间)缺页时从磁盘读入一页的耗时,约几毫秒,相当于管理员跑一趟仓库,比 t_m 慢十万倍。
- 加权求和按概率把两种时间混合,p 越大,EAT 越接近 t_d,系统就“抖动”了。
AI 应用场景
Ollama / llama.cpp 本地大模型推理
阶段:模型部署与推理服务
解决的问题:传统方式加载 7B 模型要把约 4GB 权重整体 read() 进内存,启动要几十秒,内存小的机器直接 OOM。GGUF + mmap 让进程先映射文件、再按需载入权重页,冷启动瞬间完成,小内存机器也能流畅对话。
具体案例:一台 16GB 内存的 MacBook 上运行 Ollama 加载 llama3-8B 的 GGUF 量化版,敲下命令到出现对话提示不到 1 秒。打开活动监视器会发现 RSS 只有几百 MB,其余权重页在推理过程中才陆续进入页缓存。
多进程 API 服务共享模型权重
阶段:高并发推理服务
解决的问题:传统方式下每个 worker 进程各自读一份权重,8 个 worker 就要 8 份物理内存,成本极高。mmap 后多个 worker 映射同一 GGUF 文件,缺页时共享同一份页缓存,物理内存近似只占一份。
具体案例:一个基于 fork 的 FastAPI 推理服务,master 启动时 mmap 模型文件,随后 fork 出 8 个 worker。运维用 free -m 观察,发现 8 个进程的 RSS 总量远小于 8 份模型体积,内存成本几乎等于一份权重。
模型转换与量化工具链
阶段:模型压缩与格式转换
解决的问题:把 HuggingFace 权重转成 GGUF 或做量化校准,需要反复读取完整权重,磁盘 IO 和时间成本高。mmap 可以按需读入参与计算的权重块,避免整文件载入,峰值内存远小于模型体积。
具体案例:使用 llama.cpp 的 convert_hf_to_gguf.py 将 13B 模型转换为 GGUF,在 32GB 内存的机器上直接 mmap 原模型逐层转换,峰值内存远低于一次性载入 26GB 权重的传统做法。
Python 演示
import mmapimport struct def build_demo_gguf(path, rows, cols): """构造一个只含一个张量的最小 GGUF 文件,用于演示 mmap 读取。 布局:magic(4) + version(4) + tensor_count(8) + kv_count(8) + 张量信息表 + 张量数据区 """ header = b"GGUF" + struct.pack("<I", 3) header += struct.pack("<Q", 1) # tensor_count header += struct.pack("<Q", 0) # kv_count name = b"token_embed.weight" n_bytes = rows * cols * 4 tensor_info = struct.pack("<Q", len(name)) + name tensor_info += struct.pack("<I", 2) # n_dims tensor_info += struct.pack("<Q", rows) + struct.pack("<Q", cols) tensor_info += struct.pack("<I", 0) # ftype = float32 data_offset = len(header) + len(tensor_info) + 8 # 预留 offset 字段 tensor_info += struct.pack("<Q", data_offset) weights = bytes((i * 7) % 256 for i in range(n_bytes)) with open(path, "wb") as f: f.write(header + tensor_info + weights) def read_tensor(path, target_name): """通过 mmap 按需读取 GGUF 中的指定张量,不把整个文件载入物理内存。""" with open(path, "rb") as f: with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm: if mm[:4] != b"GGUF": raise ValueError("不是合法的 GGUF 文件") t_count = struct.unpack("<Q", mm[8:16])[0] pos = 24 # 跳过 magic/version/tensor_count/kv_count for _ in range(t_count): name_len = struct.unpack("<Q", mm[pos:pos + 8])[0] pos += 8 name = mm[pos:pos + name_len].decode("utf-8") pos += name_len n_dims = struct.unpack("<I", mm[pos:pos + 4])[0] pos += 4 fmt = "<" + "Q" * n_dims dims = struct.unpack(fmt, mm[pos:pos + 8 * n_dims]) pos += 8 * n_dims ftype = struct.unpack("<I", mm[pos:pos + 4])[0] pos += 4 offset = struct.unpack("<Q", mm[pos:pos + 8])[0] pos += 8 if name == target_name: size = dims[0] * dims[1] * 4 raw = mm[offset:offset + size] # 零拷贝切片 head = struct.unpack("<4f", raw[:16]) print("命中张量 %s,ftype=%d,偏移=%d" % (name, ftype, offset)) print("前 4 个权重值:", head) return raw return None if __name__ == "__main__": path = "demo.gguf" build_demo_gguf(path, rows=4, cols=4) data = read_tensor(path, "token_embed.weight") assert data is not None and len(data) == 64 print("mmap 按需读取成功,仅保留 64 字节权重切片")
mmap.mmap 只读映射它,按张量信息表中的偏移量零拷贝取出目标权重,完整演示“按需加载 + 偏移定位”。with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm:把整个文件映射为只读虚拟地址空间,不加载任何内容到物理内存。if mm[:4] != b"GGUF":读取魔数,验证文件是否为合法 GGUF,像检查借阅卡真伪。pos = 24跳过固定 24 字节头部,定位到张量信息表起点。name = mm[pos:pos + name_len].decode("utf-8")从映射中直接读取张量名字节再解码。fmt = "<" + "Q" * n_dims按维度数量动态构造小端无符号 64 位整数解析格式。offset = struct.unpack("<Q", mm[pos:pos + 8])[0]读出张量数据在文件中的绝对偏移。raw = mm[offset:offset + size]零拷贝切片,直接从页缓存拿到权重字节,不做复制。
常见误区
误区我用 mmap 映射了一个 7B 模型文件,任务管理器里显示内存暴涨,说明文件已经全部占用了物理内存,小内存机器根本没法用 mmap。
正解任务管理器里暴涨的是“虚拟内存”(VSZ),不是“物理内存”(RSS)。mmap 只是在虚拟地址空间里做了登记,物理内存只加载你真正访问过的页。像图书馆借阅卡,你看到 200 卷书脊(虚拟映射)不等于 200 卷都堆在你桌上(物理加载)。判断真实占用要看 RSS。
为何常见:大多数人把“内存”当成一个笼统概念,混淆了虚拟地址空间与物理内存。看到监控数字大就以为物理内存被吃满,忽略了按需加载这一核心机制。
误区大模型推理时迟早要访问大部分权重,按需加载只会触发一堆缺页中断拖慢速度,不如启动时用 read() 全部读进内存,一劳永逸。
正解这是把“启动读入”和“按需加载”对立起来了。mmap 的页缓存同样会保留已读页,顺序推理时缺页只发生一次,后续全是页缓存命中;而一次性 read 会把启动延迟从几百毫秒拉长到几十秒,且冷启动期间内存压力巨大。对 serverless 或多副本部署,“秒级启动”是刚需。
为何常见:人们习惯用自己的桌面软件经验推断服务端场景,忽略了启动延迟和内存峰值在部署中的代价。
误区mmap 之后我直接修改指针指向的内容,文件就自动改好了,省去了写文件的麻烦,所以共享映射随便用,往里面写东西没问题。
正解读写权限和共享策略是两回事。GGUF 推理用 ACCESS_READ 只读映射,写入会触发 SIGSEGV 段错误;即便是 MAP_SHARED 可写映射,写入也要经过页缓存回写磁盘,多个进程同时写还会互相覆盖。模型文件应始终只读映射,防止推理时手滑破坏权重。
为何常见:把“能映射”误解为“能随便读改写”,忽略了权限标志和共享映射的并发语义。
误区mmap 这么神,我把 70B 模型转成 GGUF 再 mmap,1GB 内存的机器也能跑,反正能按需加载。
正解mmap 能延迟加载,但物理内存和页缓存总量仍有上限。如果推理实际需要访问的工作集超过物理内存,系统会陷入页抖动——管理员疲于奔命,吞吐直接崩盘。此外 32 位系统地址空间只有 4GB,映射超大文件会直接失败。mmap 是优化,不是魔法。
为何常见:只看到“虚拟内存能映射大文件”的表面,忽略了物理内存是缓存命中的基础,以及地址空间位数的硬约束。
面试题
Q1. mmap 把一个文件映射到进程的哪个空间中?
- A.物理内存中
- B.虚拟地址空间← 正确答案
- C.磁盘缓冲区
- D.文件系统缓存
答案:B。mmap 建立的是文件与进程虚拟地址空间的映射关系,并未把数据搬进物理内存。就像借阅卡让你“看到”整排书架(虚拟空间),书页(物理内存)要等访问时才送。A 是结果而非动作,C、D 与进程地址空间无关。
Q2. GGUF 文件头的固定魔数(magic)是什么?
- A."TFLO"
- B."PTNE"
- C."GGUF"← 正确答案
- D."SAFE"
答案:C。GGUF 文件前 4 字节固定为 GGUF,用于识别文件格式合法性。解析器先读魔数,就像图书馆先验证借阅证编号,不匹配就直接拒绝,避免把其他格式当 GGUF 解析导致崩溃。
Q3. 进程访问一个已映射但尚未加载的虚拟页时,操作系统会触发什么?
- A.立即崩溃退出
- B.触发缺页中断← 正确答案
- C.返回空指针
- D.等待应用重试
答案:B。CPU 查页表发现虚拟页没有对应物理页,会产生缺页中断,内核从磁盘读入该页并更新页表,程序随后继续执行。这是 mmap 按需加载的核心机制,如同管理员听到服务铃才去仓库取书,而不是让读者崩溃。
Q4. mmap 相比传统 read() 读取大模型权重的最大优势是什么?
- A.磁盘读速度更快
- B.零拷贝按需加载← 正确答案
- C.不需要打开文件
- D.自动加密权重
答案:B。read() 要把文件内容先拷贝到用户缓冲区,且一次读多少就得准备多少内存;mmap 让程序直接访问页缓存中的原始字节(零拷贝),并且只在实际访问时才加载对应页(按需)。磁盘速度由硬件决定,mmap 并不改变它。
Q5. 判断 mmap 映射的大模型权重是否真的占用物理内存,应重点观察哪个指标?
- A.VSZ 虚拟内存高
- B.RSS 常驻内存低← 正确答案
- C.磁盘空间剩余
- D.CPU 核心数量
答案:B。VSZ 记录虚拟地址空间大小,mmap 后必然很大,但虚拟内存不等于物理占用;RSS 才统计真正驻留在物理内存中的页。想看“书页有没有真的送到桌上”,当然要看书桌上的书(RSS),而不是书架投影(VSZ)。
Q6. 当 mmap 的文件被外部截断,且程序访问到缺失区域时,进程会发生什么?
- A.收到 SIGBUS 信号← 正确答案
- B.收到 SIGSEGV 信号
- C.自动重读文件
- D.静默返回零值
答案:A。访问映射区域内但文件已不存在的页,内核无法从磁盘读取,会发 SIGBUS(总线错误)终止进程。SIGSEGV 是非法地址访问,通常是解引用空指针或越界;而“页存在但文件缺页”属于总线错误,如同翻到一页被撕掉的书。
Q7. 在 32 位系统上 mmap 一个 4GB 的 GGUF 模型文件,最可能的结果是什么?
- A.完全正常加载
- B.地址空间不足失败← 正确答案
- C.自动切分映射
- D.提升为 64 位
答案:B。32 位系统虚拟地址空间只有 4GB,除去内核占用和进程自身代码堆栈,用户可用空间通常不足 3GB,映射 4GB 文件会因地址空间不够而失败。所以大模型 GGUF 部署必须依赖 64 位系统。
Q8. 下列哪些是 GGUF 使用 mmap 带来的实际好处?
- A.冷启动只需读头部← 正确答案
- B.权重页按需载入← 正确答案
- C.多进程共享页缓存← 正确答案
- D.显存占用必然降低
答案:ABC。mmap 让冷启动只读文件头和张量信息表,权重页在访问时才缺页载入,多个进程映射同一文件时共享页缓存,这些都是实打实的收益。但 mmap 只影响内存,不改变显存占用,D 说法绝对化,错误。
Q9. 关于 mmap 的页缓存共享行为,下列说法正确的有哪些?
- A.fork 后父子进程共享已映射页← 正确答案
- B.两进程映射同一文件共享物理页← 正确答案
- C.页缓存可被内核回收后重建← 正确答案
- D.共享映射必然损坏原文件
答案:ABC。fork 后子进程继承父进程页表,共享已映射页;两个进程映射同一文件时,内核页缓存只保留一份物理副本;内存紧张时内核可回收干净页,再访问时重新从磁盘读出。共享是否损坏文件取决于读写权限,只读映射不会损坏,D 错误。
Q10. 大模型推理场景下使用 mmap 时,常见的风险有哪些?
- A.内存吃紧引发页抖动← 正确答案
- B.冷启动首访带来 IO 阻塞← 正确答案
- C.mmap 能让任意模型无视内存限制
- D.文件被截断时访问触发异常← 正确答案
答案:ABD。工作集超过物理内存会导致页抖动,冷启动首次访问缺页会造成同步 IO 阻塞,文件被截断后访问缺失页会触发 SIGBUS,这些都是 mmap 部署的真风险。mmap 能延迟加载但不能突破物理内存上限,C 说法错误。