码农高天
Python通过Block与Pool三层结构托管512字节以下内存,仅在整个Arena彻底放空时才归还系统,导致进程内存占用释放极为迟缓。
AI-assisted summary of 【python】内存管理结构初探——我要的内存从哪儿来的? — original source. Check the original for full context.
Source published:
Key ideas
- Python 针对 512 字节(SMALL_REQUEST_THRESHOLD)设立了分水岭:大于该阈值的大内存直接调用 C 语言 malloc 向操作系统申请,而小内存则进入内部三层架构管理,以避免频繁系统调用与内存碎片化。 (0:00)
- 小内存管理采用 Block(固定大小数据块)、Pool(4KB 内存页)和 Arena(256KB 内存区)三层结构,并通过 8 字节内存对齐与 usedpools 数组实现极速检索与外部碎片消除。 (5:00)
- 释放小对象时内存仅退回 Pool 的内部空闲链表,操作系统无法感知;现代 Python 仅在整整 256KB 的 Arena 内所有 Block 完全清空时,才会真正向操作系统执行 free。 (10:00)
- 为提高内存归还效率,Python 调度分配时会优先使用最满的 Arena,促使低利用率 Arena 尽早彻底腾空以触发物理内存归还。 (10:00)
Detailed notes
## Background 该视频由 YouTube 频道“码农高天”于 2024 年 4 月 6 日发布。视频简介说明,本期内容将从高层视角(high level)简要探讨 Python 基础的底层内存管理结构,拆解 Python 究竟是如何分配与管理内存的。
## TL;DR 视频深入剖析了 Python 申请与释放内存的底层机制。绝大多数对象创建均通过 PyObject_Malloc,并以 512 字节为界:大于 512 字节的大内存直接交由 C 语言 malloc 向操作系统申请;而小于等于 512 字节的小内存则走内部专属的 pymalloc 架构。该架构通过 Block、4KB 的 Pool 以及 256KB 的 Arena 三层层级结构与 usedpools 索引数组进行 8 字节对齐管理,以消除碎片化并规避频繁系统调用的开销。在释放时,小对象仅归还到 Pool 的空闲链表而不会通知操作系统,只有当整个 Arena 完全空闲时才会统一归还给系统。
## Atmosphere & style 讲解风格通俗务实、条理极其严密,展现了硬核程序员拆解底层代码时的从容与笃定。高天善于运用生动的类比(如“排排队”拿固定规格内存)化繁为简,将抽象复杂的 C 语言底层指针与分页机制讲得清晰易懂。整段解析既有自顶向下的架构全景,又有微观到 8 字节对齐的细节印证,听来让人产生一种“知其然更知其所以然”的顿悟感与技术踏实感。
## Bulletin - [00:00](https://www.youtube.com/watch?v=igB-hkESMeI&t=0s) Python 针对 512 字节(SMALL_REQUEST_THRESHOLD)设立了分水岭:大于该阈值的大内存直接调用 C 语言 malloc 向操作系统申请,而小内存则进入内部三层架构管理,以避免频繁系统调用与内存碎片化。 - [05:00](https://www.youtube.com/watch?v=igB-hkESMeI&t=300s) 小内存管理采用 Block(固定大小数据块)、Pool(4KB 内存页)和 Arena(256KB 内存区)三层结构,并通过 8 字节内存对齐与 usedpools 数组实现极速检索与外部碎片消除。 - [10:00](https://www.youtube.com/watch?v=igB-hkESMeI&t=600s) 释放小对象时内存仅退回 Pool 的内部空闲链表,操作系统无法感知;现代 Python 仅在整整 256KB 的 Arena 内所有 Block 完全清空时,才会真正向操作系统执行 free。 - [10:00](https://www.youtube.com/watch?v=igB-hkESMeI&t=600s) 为提高内存归还效率,Python 调度分配时会优先使用最满的 Arena,促使低利用率 Arena 尽早彻底腾空以触发物理内存归还。
## Key takeaways - [00:00](https://www.youtube.com/watch?v=igB-hkESMeI&t=0s) 绝大多数 Python 自定义类对象在内存申请时均走 PyType_GenericAlloc,并最终汇聚至底层核心入口 PyObject_Malloc(_PyObject_Malloc)。 - [00:00](https://www.youtube.com/watch?v=igB-hkESMeI&t=0s) Python 以 512 字节为界限:大于 512 字节通过 PyMem_RawMalloc 直接调用系统 malloc,小于等于 512 字节则进入 pymalloc 专属小内存分配体系。 - [00:00](https://www.youtube.com/watch?v=igB-hkESMeI&t=0s) Python 运行中会频繁创建与销毁诸如临时字典、列表等微小对象,若全部直调 malloc 会因频繁系统调用产生巨大开销并带来严重的内存碎片化。 - [05:00](https://www.youtube.com/watch?v=igB-hkESMeI&t=300s) Python 小内存层级由 Block(最小数据块)、Pool(4KB,对应操作系统内存页)与 Arena(256KB,默认预分配 16 个)三层容器构成。 - [05:00](https://www.youtube.com/watch?v=igB-hkESMeI&t=300s) Pool 内部的所有 Block 规格必须完全一致,但不同规格 Pool 之间的尺寸由其激活转为“使用中”时动态确定,从而彻底消除了物理不连续导致的外部碎片。 - [05:00](https://www.youtube.com/watch?v=igB-hkESMeI&t=300s) Python 对小内存统一实施 8 字节向上对齐(例如 1-8 字节对齐为 8 字节,22 字节对齐为 24 字节),因此 Python 内部对象的字节大小始终可被 8 整除。 - [05:00](https://www.youtube.com/watch?v=igB-hkESMeI&t=300s) 核心结构 usedpools 本质是一个按照 8 字节步长索引的 Pool 指针数组,内存申请时仅需规整尺寸后取索引即可直接拿到空闲 Block,大幅提升分配效率。 - [10:00](https://www.youtube.com/watch?v=igB-hkESMeI&t=600s) 释放微小对象时,系统仅是将 Block 放回所属 Pool 的空闲链表以备下次复用,操作系统完全感知不到内存的释放操作。 - [10:00](https://www.youtube.com/watch?v=igB-hkESMeI&t=600s) 早期 Python 对 Arena 体系的内存只增不减;现代 Python 虽支持归还,但向操作系统申请与归还的最小物理单位是 256KB 的 Arena。 - [10:00](https://www.youtube.com/watch?v=igB-hkESMeI&t=600s) 只要某个 Arena 内存在任何一个 Block 仍在使用,整个 Arena 就无法向操作系统释放,这解释了为何 Python 进程占用的物理内存在宏观上归还极为迟缓。 - [10:00](https://www.youtube.com/watch?v=igB-hkESMeI&t=600s) Python 通过将分配压力集中在“最满”的 Arena 上,促使低利用率的 Arena 尽快彻底清空并归还系统,在复用内存与控制进程膨胀之间取得平衡。
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=igB-hkESMeI&t=0s) Python 申请内存的入口与为什么不直接用 malloc
绝大部分自定义 class 在申请内存时,走到的都是 `PyType_GenericAlloc` 函数。该函数首先会判断目标类型是否为一个容器对象、是否存在循环引用的可能;如果是容器,Python 需要对其做额外处理以防内存泄漏。但不管是不是容器,Python 申请内存的核心入口最终都会汇聚到 `PyObject_Malloc`,并进一步调用内部的底层实现 `_PyObject_Malloc`。
在这个正式的申请入口处会出现一个分水岭:系统会先尝试通过 `pymalloc_alloc` 申请内存,若失败则退回到 `PyMem_RawMalloc`。绝大多数申请“失败”并非内存耗尽,而是因为检查条件命中——即申请字节数 `nbytes` 是否大于小内存阈值(`SMALL_REQUEST_THRESHOLD`,固定为 512 字节)。当请求的内存大小大于 512 字节时,Python 就会走 `PyMem_RawMalloc`,其底层非常纯粹,直接调用 C 语言的 `malloc`,需要多少就直接管操作系统要多少。
很多人会下意识认为 Python 创建任何一个对象都是直接去向操作系统 `malloc`,但码农高天指出事实并非如此:“因为我们知道 Python 这个语言在运行的过程中会建立非常多小的 object,比如说在函数参数调用的过程中,它就会建立小的 dictionary 或者小的 list。”如果频繁为这些微小对象直接调用系统 `malloc`,不仅会因为潜在的系统调用(System Call)产生巨大的开销(overhead),更严重的是会产生海量的内存碎片。Python 具有创建后立刻释放、频繁分配与销毁的语言特性,反复向系统索取和归还小块内存会导致内存极度碎片化,因此 Python 必须在底层建立一套专属的内部内存管理系统。
```mermaid flowchart TD accTitle: Python 内存分配入口逻辑 accDescr: 描述 Python 对象内存申请时判断大小并分流到专属内存池或系统 malloc 的过程
subgraph Entry ["内存申请入口"] A["PyType_GenericAlloc"] --> B["PyObject_Malloc / _PyObject_Malloc"] end
subgraph Decision ["大小判断"] B --> C{"申请大小 <= 512 bytes?"} end
subgraph Allocators ["分配路径"] C -->|"是 (小内存)"| D["pymalloc_alloc<br>(内部三层架构管理)"] C -->|"否 (大内存)"| E["PyMem_RawMalloc<br>(直接调用 C 语言 malloc)"] end ```
- `PyType_GenericAlloc`:绝大多数自定义 class 分配内存的统一入口。 - `SMALL_REQUEST_THRESHOLD`:Python 小对象内存分配的阈值,固定为 512 字节。 - 大内存(> 512 字节):由 `PyMem_RawMalloc` 直通 C `malloc` 处理。 - 小内存(<= 512 字节):进入 Python 专属的小内存分配系统(pymalloc)。
---
[05:00](https://www.youtube.com/watch?v=igB-hkESMeI&t=300s) 三层层级结构:Block、Pool、Arena 与 usedpools
Python 的小内存管理系统由小到大分为三层容器结构: 1. **Block(数据块)**:最小的单位,每一个 block 保存固定大小的数据。 2. **Pool(内存池)**:每个 pool 大小通常为 4KB,刚好对应操作系统的一个内存页(Page)。一个 pool 内的所有 block 大小完全相同,因此每个 pool 会维护一个 `size` 属性。不同 pool 之间的 block 规格可以不同,但单一 pool 内部的 block 规格是严格一致的。 3. **Arena(内存区)**:每个 arena 的默认大小为 256KB。一个 arena 内部包含多个 pool。在初始状态下,arena 主要记录未被使用的 pool。特别的是,pool 内部的 block 尺寸并不是一开始就固定的,而是在该 pool 从“未使用”转变为“使用中”的那一刻才动态决定的。换言之,所有 pool 的物理总大小一致,但由于内部划分的 block 尺寸不同,所能容纳的 block 数量也不同。Python 启动时会预先建立一堆 arena(通常是 16 个),用尽后才会申请新的 arena。
管理这套架构的核心枢纽是 `usedpools`。可以将其理解为一个存放 pool 指针的数组,该数组的索引(index)直接与 block 的尺寸绑定: - `index 0` 对应的 pool,其内部每个 block 为 8 字节; - `index 1` 对应的 pool,其内部每个 block 为 16 字节; - `index 2` 对应的 pool,其内部每个 block 为 24 字节;以此类推。
当小内存申请到来时,Python 首先进行“内存对齐”(Memory Alignment):申请 1 到 8 字节统一给 8 字节,申请 9 到 16 字节统一给 16 字节。码农高天强调:“所以当你看 python 里面那个 object 的大小的时候,你会发现它的 byte 数永远是可以被 8 整除的。”对齐后,系统直接根据规整后的大小取索引,去 `usedpools` 对应的 pool 里面随便取一个空闲 block 分配出去。例如申请 22 字节,对齐后为 24 字节,Python 就会精准定位到 `index 2` 的 pool,直接摘下一个未使用的 24 字节 block。
之所以采用如此严格的分类机制,是为了彻底解决碎片化问题。如果随意连续申请 10 块 50 字节的内存,随后将第 1、3、5、7、9 块释放,内存空间就会变得千疮百孔;此时若想申请 100 字节,即使空闲总和有 250 字节,也因为物理不连续而无法使用,只能重新开辟。而 Python 的逻辑是“排排队”:申请 32 字节就去 32 字节规格的池子里拿,释放时也还回该池子;由于池内每个 block 规格一样,随便拿出一个都能满足需求,既消除了外部碎片,又大幅提升了分配速度。
```mermaid flowchart TD accTitle: Python 内存管理三层容器层级 accDescr: 展示 Arena、Pool、Block 三层包含关系以及 usedpools 索引映射机制
subgraph ArenaLevel ["Arena 层 (256 KB)"] AR["Arena (预分配 16 个)"] end
subgraph PoolLevel ["Pool 层 (4 KB / 内存页)"] AR --> P1["Pool A (8B blocks)"] AR --> P2["Pool B (24B blocks)"] AR --> P3["Pool C (未激活/规格未定)"] end
subgraph UsedPoolsArray ["usedpools 索引数组"] UP0["index 0 (8 Bytes)"] -.-> P1 UP2["index 2 (24 Bytes)"] -.-> P2 end
subgraph BlockLevel ["Block 层 (数据块)"] P1 --> B1["Block (8B)"] P1 --> B2["Block (8B)"] P2 --> B3["Block (24B)"] end ```
- Pool 大小:4KB,与单个内存页等大。 - Arena 大小:256KB,Python 初始化时默认建立 16 个。 - 内存对齐规则:按 8 字节向上取整(例如 5 字节对齐为 8 字节,22 字节对齐为 24 字节),对象占用的字节数必然能被 8 整除。
---
[10:00](https://www.youtube.com/watch?v=igB-hkESMeI&t=600s) 对象的释放机制与 Arena 的回收策略
当 Python 创建一个小对象并获取内存时,拿到的是属于某个 pool 的 block,而该 pool 归属于某个 arena。这种预先批发的架构让获取小内存时的开销变得极低,不需要频繁打扰操作系统。
然而在对象释放(free)时,这种机制也带来了一个关键现象:当用户释放一个 Python 对象,系统仅仅是将该 block 放回所属 pool 的 free block 空闲链表里,标记为“又可以使用了”,并没有将其真正归还给操作系统。码农高天总结道:“换句话说,在绝大部分情况下,当你在 Python 里面建立一个 object、销毁一个 object,如果这个 object 比较小,操作系统压根就不知道。”
这引出了一个核心疑问:Python 是否会一直霸占这些内存不归还? - **早期版本**:走 arena 这一套体系的内存是只增不减、从来不向操作系统执行 `free` 的(走 `raw malloc` 的大内存除外)。 - **现代版本**:现代 Python 已经支持向操作系统释放内存,但其释放与申请的最小物理颗粒度都是以 **Arena** 为单位的。“也就是说,但凡你这个 arena 里面有一个 pool 的一个 block 还正在被使用,这个 arena 就没有办法被 free 掉。”
为了改善回收效率,Python 在内部做了一定优化:在需要申请新的 pool 时,会优先从“最满的”(即使用率最高、剩余空闲 pool 最少)arena 里面分配。通过对所有 arena 进行排序调度,Python 尽量把分配压力集中在部分 arena,从而让那些使用比例极小的 arena 更有机会彻底清空所有 block,进而被整体归还给操作系统。
只要没有将所有已存在 arena 中的 pool 全部耗尽,Python 就不会申请新的 arena,而是反复复用现有内存。因此这一机制并不会导致进程内存无休止膨胀,但确实会导致一个直观体验:Python 真正把物理内存还给操作系统的节奏往往十分迟缓。
- 归还逻辑:小对象的 free 只是把 block 归还给 pool 的内部空闲链表,操作系统无法感知。 - 操作系统级 Free:只有当整个 256KB 的 arena 内的所有 block 全部空闲时,该 arena 才会真正调用系统 free。 - 排序优化:优先向使用率最高的 arena 索取新的 pool,促使低利用率 arena 尽快清空以释放物理内存。
## Notable quotes > [10:00](https://www.youtube.com/watch?v=igB-hkESMeI&t=600s) 在绝大部分情况下,当你在 Python 里面建立一个 object、销毁一个 object,如果这个 object 比较小,操作系统压根就不知道。 — 码农高天
> [10:00](https://www.youtube.com/watch?v=igB-hkESMeI&t=600s) 但凡你这个 arena 里面有一个 pool 的一个 block 还正在被使用,这个 arena 就没有办法被 free 掉。 — 码农高天
## Mentioned 码农高天, Python, PyType_GenericAlloc, PyObject_Malloc, _PyObject_Malloc, pymalloc_alloc, PyMem_RawMalloc, malloc, free, usedpools, Block, Pool, Arena, C 语言
## Video description 这期视频我们聊一下基础的python内存管理结构,从high level的角度简单聊一下,python是如何管理内存的。
[Source](https://www.youtube.com/watch?v=igB-hkESMeI)