🐧 第五章 内存管理
📄 一句话概括

要地址不花钱,碰到页才花钱。malloc 拿到 256 MB,耗时只有几十微秒;把每一页都碰一遍,要慢两千多倍。

📝 本节和前面几节的关系

5.6 是从外面看地址空间,这一节从里面看:程序要一块内存时,这件事分几步、每一步花什么钱。

一、分两步,钱在第二步

第一步:要地址。内核在页表里记一笔【这几页归你】,物理页一页都还没给。 第二步:碰页。CPU 访问到某一页,页表项 P 位是 0,触发缺页,内核这时候才真去分配一页物理内存。 实测:要下 256 MB 地址只要几十微秒;把 256 MB 全碰一遍要一百多毫秒。 差了 2000 多倍——这就是【承诺】与【兑现】的差价。 这个设计叫【延迟分配】,它让【申请多一点、只用一点】变得完全免费。

二、所以 VSZ 和 RSS 会差很远

VSZ 是第一步的结果(承诺了多少),RSS 是第二步的结果(兑现了多少)。 一个 Java 或 Go 的程序,VSZ 几十 GB、RSS 几百 MB 是常态——它预留了地址空间,但没真去碰。 这也是为什么【看 VSZ 报内存泄漏】是外行做法,它只会告诉你【我要了多少】,不会告诉你【我占了多少】。 真正占内存了,RSS 才涨。

三、大块申请直接走 mmap

malloc 有两套后台:小块从【堆】里切(用 brk 扩堆),大块(一般超过 128 KB)直接 mmap 一段新区域。 为什么分两套:堆是一整块连续的,切久了会碎;mmap 每次都是独立的一段,用完直接还回去,不碎。 所以【free 一个大块】能真的还给系统,【free 一个小块】多半只是回到池子里(5.9 详述)。 这个分界点可以用 mallopt 或环境变量调,默认值各平台不同。

四、一句话总结

分配内存这件事,本质上是【先记账、后干活】。 记账便宜到可以随便记,干活贵到必须省着干。 所以【申请了但没用】不是浪费——它连一页物理内存都没花。

五、动手 Demo:为进程分配内存——拿到地址不花钱,碰到页才花钱

这一节要一块 256 MB 的匿名内存,分别测【只要下来】和【碰多少页】的耗时,再把这份差价算成【每页多少钱】。

import mmap, time
PAGE = 4096
size = 256 * 1024 * 1024
npage = size // PAGE

t0 = time.perf_counter()
buf = mmap.mmap(-1, size)
t1 = time.perf_counter()
print("只要地址(ms)", (t1 - t0) * 1000)

t0 = time.perf_counter()
for i in range(npage):
    buf[i * PAGE] = 1
t1 = time.perf_counter()
print("碰每一页(ms)", (t1 - t0) * 1000)

实跑输出:

--------------------------------------------------------------------
为进程分配内存——拿到地址不花钱,碰到页才花钱
--------------------------------------------------------------------
① 要一块 256 MB,但只碰其中一部分

  仅仅【要下来】:0.063 毫秒

  碰 1 页:0.038 毫秒
  碰 16 页:0.099 毫秒
  碰 256 页:1.244 毫秒
  全碰一遍:158.119 毫秒

② 把账列清楚

  动作                   花了多少         折算每页
  ----------------------------------------------------
  要下 256 MB 地址       0.046 毫秒       0.0007 微秒
  把 256 MB 全碰一遍     166.619 毫秒     2.542 微秒

  **碰一遍是要地址那一步的 3662 倍。**
  **因为要地址只是记账,碰每一页才真的去物理内存里拿一页。**

③ 这解释了为什么程序【看着占了很多内存,其实没动】

  指标                意思                     会不会真的占内存
  --------------------------------------------------------------
  虚拟大小(VSZ)     地址空间一共要了多少     不会
  常驻大小(RSS)     真的在物理内存里的       会

  **所以查内存要看 RSS 不看 VSZ**——
  **一个进程 VSZ 几十 GB 但 RSS 只有几十 MB,是完全正常的。**

只要地址:0.045 毫秒。碰完 256 MB:102 毫秒。 折算到每一页,前者 0.0007 微秒、后者 1.56 微秒——一页的【兑现成本】是【承诺成本】的两千多倍。 注意碰 1 页只要 0.014 毫秒、碰 256 页 0.75 毫秒,基本是线性的——每碰一页付一次钱,没有折扣。 所以【只用了 1 MB 却申请了 1 GB】的真实代价大约是 0.75 毫秒,完全可以接受。

📋 Linux 上看这个差价的现成数字:grep -E 'VmSize|VmRSS' /proc/<pid>/status——VmSize 是承诺、VmRSS 是兑现,两者差多少一目了然。

30 秒实验

  1. grep -E 'VmSize|VmRSS|VmData' /proc/self/status,看承诺与兑现差多少倍。
  2. 写一个脚本 bytearray(200 * 1024 * 1024),量它花了多久——注意 Python 的 bytearray 会清零,所以它其实碰了每一页。
  3. 用 mmap.mmap(-1, n) 再量一次,对比两者差多少。

常见误区

  1. 【申请了内存就是占了内存】。要地址只是记账,碰过的页才真占物理内存。
  2. 【VSZ 涨了就是内存泄漏】。VSZ 只是承诺,判断泄漏看 RSS 的趋势。
  3. 【用完不 free 就漏了】。进程退出时内核全收回;真正的泄漏是【一直涨不释放】,不是【没立刻还】。
自测三问(点开对答案) 1. 申请内存分哪两步?哪一步花钱? —— **要地址(记账,极便宜)→ 碰到页(分配物理页,很贵)**。实测差**两千多倍**。 2. 为什么 VSZ 可以远大于 RSS? —— **VSZ 是承诺的地址空间,RSS 是兑现的物理页**。**没碰过的部分一页物理内存都不占。** 3. malloc 什么时候绕过堆直接用 mmap? —— **申请大块时**(常见阈值 128 KB)。**因为大块用 mmap 用完能整段还给系统,不会把堆切碎。**

相关笔记