一个进程的地址空间不是一个整块,是分段的:代码、只读数据、已初始化数据、堆、匿名映射、栈、内核。堆往上长、栈往下长,中间是巨大的空洞。
5.5 讲完了页表这个机制,这一节换个角度:从外面看一个进程到底占了哪些段。这是排查内存问题最实用的一个视角。
一、先看现象:地址是散着的
在同一个进程里连续创建五个小对象,打印它们的地址。 结果地址横跨好几 MB,完全不是紧挨着的。 原因:分配器把不同大小的对象放在不同的池子里,池子之间隔着几 MB 的记账空间。 所以【对象的地址】这件事,程序员基本不该假设任何东西——除了【同一个对象地址不变】。
二、Linux 上的六段
打开 /proc/
三、堆往上长、栈往下长
这是为了共用中间那片空白:两边对着长,谁也别提前撞上谁。 中间那片巨大的空洞不占物理内存——因为对应的页表项根本没建。 所以【VSZ 几十 GB】完全可以只有【RSS 几十 MB】。 栈往下长还有一个好处:递归深了自动就撞到保护区,触发栈溢出报错,而不是悄悄改掉别的数据。
四、这个视角有什么用
查内存涨:看是哪一段在涨——堆在涨是程序分配多了,匿名映射在涨可能是线程栈或 mmap。 查段错误:崩在哪一段,就知道是访问代码、数据还是栈。 查共享:权限位是 c(共享)的段,是多个进程共用同一份物理内存。 一句话:maps 是进程内存的【户口本】。
五、动手 Demo:看看一个进程的地址空间长什么样
这一节先在沙箱里打印几个对象的地址(看它们散得多开),再把 Linux 上 /proc/
objs = [bytearray(16) for _ in range(5)]
for i, o in enumerate(objs, 1):
print(i, hex(id(o)))
addrs = [id(o) for o in objs]
print("跨度(MB)", (max(addrs) - min(addrs)) / 1024 ** 2)
实跑输出:
--------------------------------------------------------------------
看看一个进程的地址空间长什么样
--------------------------------------------------------------------
① 同一个程序里的对象,地址是散着的
第几个 地址(十六进制) 与上一个差多少
-----------------------------------------------
#1 0x22256B03130 —
#2 0x22256B031B0 128
#3 0x222549E0DB0 34743296
#4 0x222549E36F0 10560
#5 0x22254ACBD70 951936
**五个 16 字节的对象,地址横跨了 33.13 MB**——完全不是紧挨着的。
**它们被分在不同的内存块里,中间的空隙是分配器自己留的记账空间**
(和 5.9、5.10 讲的是同一件事)。
② Linux 上对应的是 /proc/<pid>/maps
地址段 权限 用途
---------------------------------------------------------
低地址一小段 r--p 可执行文件本身(代码)
接着一段 r--p 只读数据、字符串常量
再一段 rw-p 已初始化的全局变量
再一段 rw-p 堆(malloc 从这里要)
一长条 rw-p 匿名映射(大块 malloc 走这里)
很高的一段 rw-p 栈(往下长)
最高处 r-xp 内核的代码(所有进程都一样)
**注意堆往上长、栈往下长,中间留出巨大的空洞**——
**空洞不占物理内存,因为对应的页表项根本没建。**
③ 自己动手看一眼
📋 **Linux**:`cat /proc/self/maps` 或 `pmap -x <pid>`。
📋 **筛选只看堆和栈**:`grep '\[heap\]\|\[stack\]' /proc/self/maps`。
**Windows 上对应的是 VMMap(微软官方工具)**,或 PowerShell 的
`Get-Process -Id $PID | Select-Object -ExpandProperty Modules`。
五个 16 字节的对象,地址横跨了好几 MB。 这不是【内存浪费】,是分配器按大小分池的必然结果——同一尺寸的对象才会被放在一起。 所以不要写【假设某两个变量地址相邻】的代码,任何语言都不要。 Windows 上等价的工具是 VMMap,Linux 上是 /proc/maps 与 pmap;本节的六段表就是 pmap 的读法。
📋 Linux 上一条命令看清分段:pmap -x <pid>——每一段的起始、大小、RSS、脏页数都列出来。RSS 那一列加起来,就是这个进程真占的物理内存。
30 秒实验
cat /proc/self/maps,数一数分了多少段。pmap -x $$ | tail -3,看最后那个 total 的 RSS 是多少。grep stack /proc/self/maps,看栈的范围有多大(一般 8 MB)。
常见误区
- 【一个进程的内存是一整块】。它是分段的,段与段之间隔着巨大的空洞。
- 【相邻变量的地址相邻】。分配器按大小池分配,相邻的声明完全可能差出几 MB。
- 【地址空间大就是占内存多】。空洞不占物理页,要看 RSS 不是 VSZ。
自测三问(点开对答案)
1. 一个进程的地址空间大致分哪几段? —— **代码、只读数据、已初始化数据、堆、匿名映射、栈、内核**。**堆往上长、栈往下长,中间留空洞。** 2. 为什么相邻声明的两个对象地址可能差很远? —— **分配器按【大小分类】分池子**,同类才放一起,**池子之间隔着记账空间。** 3. maps 里的权限位 `rwxp` 分别是什么意思? —— **读、写、执行、私有**(c 表示共享)。**看权限位就能知道这一段是代码、数据还是共享内存。**- ☐ 5.7 为进程分配内存
相关笔记
- 页表 — 段背后的表
- 为进程分配内存 — 地址怎么要到手
- 文件映射 — 匿名映射的另一面
- 🐧 Linux是怎样工作的 · 索引