深入 Python 内存分配原理:从对象创建到虚拟内存,彻底吃透底层机制
引言为什么同样一段 Python 代码有人跑起来内存暴涨、OOM 频发有人却轻量高效、稳如泰山为什么 Python 不像 C 那样需要手动malloc/free却依然会出现内存泄漏为什么小对象特别多时Python 内存占用会异常偏高这一切的答案都藏在Python 内存分配机制里。绝大多数 Python 开发者只停留在 “写业务” 层面对底层内存如何分配、对象如何布局、垃圾如何回收、内存池如何工作一无所知遇到内存问题只能盲目重启、瞎调 GC最终线上问题反复爆发。本文不讲空话从操作系统虚拟内存 → Python 内存架构 → PyMalloc 内存池 → 完整内存分配流程 → 对象内存布局 → 分配回收全过程 → 内存优化带你系统性吃透 Python 内存分配底层机制。看懂这一篇你对 Python 内存的理解将直接超越 90% 的后端开发者。建议点赞 收藏 关注后续持续输出 Python 底层、性能调优、高并发实战干货。一、先认清Python 内存分配的宏观视角1.1 内存分配到底在分配什么很多新手有一个误区我在 Python 里a []就是直接向物理内存申请空间。完全错误。现代操作系统都是虚拟内存机制程序申请的是虚拟内存地址操作系统通过页表映射到真实物理内存分配单位是页Page通常 4KBPython 作为上层应用不会直接跟物理内存打交道而是向操作系统申请大块虚拟内存在内部维护一套内存管理体系切割、分配、复用、回收给 Python 对象使用这就是 Python 内存分配的核心向上提供简洁接口向下管理系统内存中间做极致优化。1.2 Python 内存架构分层CPython官方标准实现的内存体系可以清晰分为四层操作系统层提供虚拟内存、mmap、brk 等系统调用Python 原生内存分配层PyMalloc、Raw malloc对象内存分配层为整数、字符串、列表、字典分配专属内存业务逻辑层我们写的a1、lst.append()等代码本文重点深入PyMalloc 内存池机制 Python 对象内存布局 完整分配与回收流程 内存优化。二、操作系统内存基础必须懂2.1 两个核心系统调用Python 最终依赖操作系统分配内存主要两个函数brk / sbrk扩展进程数据段分配小内存。mmap映射一段匿名内存适合分配大内存。Python 内部会根据对象大小自动选择分配方式小对象 → PyMalloc brk大对象 → 直接 mmap2.2 内存页与碎片操作系统以页4KB为单位管理内存。如果程序频繁申请、释放小内存会产生内存碎片。内存碎片会导致明明剩余内存总和足够却分配失败RSS 内存虚高程序运行变慢Python 设计PyMalloc 内存池核心目的之一就是减少内存碎片、减少系统调用、提升分配速度。三、Python 内存分配核心PyMalloc 全面解析PyMalloc 是 CPython 默认的内存分配器专门针对 Python 大量小对象的特点优化。3.1 为什么需要 PyMalloc原生malloc有几个致命问题频繁系统调用开销巨大Python 每秒创建数百万小对象每次都走 OS 调用性能极差。内存碎片严重小对象生命周期长短不一malloc 容易产生大量碎片。没有对象类型感知malloc 不知道内存是给 int、list 还是 dict 使用无法优化。于是 Python 设计了一套分层内存池Arena → Pool → Block3.2 PyMalloc 三层架构1Arena最大管理单位大小256 KB来源通过mmap向操作系统一次性申请一个 Arena 包含多个 PoolArena 是 Python 向 OS 申请内存的最小单位。2Pool中等管理单位大小4 KB和操作系统页大小一致一个 Pool 属于同一种 block sizePool 有三种状态used、full、emptyPool 是 PyMalloc 管理内存的核心单位。3Block最小分配单位大小从8 字节512 字节等比递增同一个 Pool 里所有 Block 大小相同分配小对象时直接从对应大小的 Pool 中取 BlockBlock 就是最终给 Python 对象使用的内存块。3.3 Block 大小分级表PyMalloc 固定了 64 种大小等级以 8 字节对齐plaintext8, 16, 24, 32, 40, 48, ..., 512 字节举例对象需要 10 字节 → 分配 16 字节 Block对象需要 30 字节 → 分配 32 字节 Block对象需要 513 字节 → 不走 PyMalloc直接走 libc malloc这就是为什么小对象多内存占用会偏高的原因 —— 内存对齐带来的内碎片。四、Python 内存分配完整详细过程从代码到物理内存这一章是全文核心把一行 Python 代码从执行到内存分配的每一步底层动作全部拆开彻底讲透。4.1 顶层触发Python 代码编译与字节码执行以最简单一行代码为例python运行lst [1, 2, 3]词法 / 语法分析解释器读取源码识别出列表创建表达式。生成 AST 抽象语法树将语法结构转为树形表示。编译为字节码最终生成类似如下字节码plaintextLOAD_CONST 1 (1) LOAD_CONST 2 (2) LOAD_CONST 3 (3) BUILD_LIST 3 STORE_FAST 0 (lst)PVM 执行字节码当执行到BUILD_LIST时正式进入对象创建与内存分配流程。4.2 进入 C 层对象分配入口函数CPython 内部所有对象创建都会通过统一入口固定大小对象int、float、小对象c运行PyObject *PyObject_New(PyTypeObject *type, size_t size);可变长对象list、str、tuple、dictc运行PyObject *PyObject_VarNew( PyTypeObject *type, size_t itemsize, Py_ssize_t nitems );以列表为例会调用c运行PyList_New(3);该函数内部做三件事计算列表对象所需总内存大小调用内存分配器申请内存初始化对象头部引用计数、类型指针4.3 判断对象大小选择分配路径Python 会严格根据申请内存大小分流路径 A小对象 ≤ 512 字节→ 走PyMalloc 内存池分配int、str、小 list、小 dict、函数、类实例等 99% 对象都走这条路径。路径 B中等对象 513 ~ 256KB→ 直接调用 libcmalloc路径 C大对象 256KB→ 直接调用 OSmmap分配匿名内存4.4 小对象分配详细流程PyMalloc 完整步骤这是 Python 最核心、最频繁的内存分配路径计算对齐大小根据对象实际大小向上对齐到 8 字节找到对应 Block 等级。查找可用 Pool从当前线程缓存查找对应大小级别的 Pool有可用 Pool 且未满 → 直接使用无可用 Pool → 从 Arena 中新建一个 Pool从 Pool 中分配 Block在 Pool 的空闲 Block 链表中取出第一个 Block将 Pool 的可用计数减 1如果 Pool 被占满标记为full移出可用队列返回 Block 虚拟地址分配器将内存地址返回给对象构造函数。初始化 PyObject 头部设置ob_refcnt 1设置ob_type指向具体类型PyList_Type初始化长度、容量等字段返回对象指针到 Python 层字节码执行完成变量绑定到该对象。4.5 内存释放流程重点不还给 OS当对象引用计数为 0 被回收时对象析构清理内部资源内存块Block归还到对应 Pool 的空闲链表Pool 标记为used或empty内存不会归还给操作系统只有当整个 Arena 全部空闲且达到阈值时才会通过munmap释放给 OS这就是 Python 内存 “只升不降” 的根本原因。4.6 多线程下的内存分配Python 为了解决 GIL 与并发分配问题为每个线程维护独立的 Pool 缓存减少多线程竞争提升小对象分配并发性能五、Python 对象内存布局与分配Python 中一切皆对象哪怕一个整数、一个布尔值都是完整的结构体。5.1 通用对象头部PyObject所有 Python 对象都有一个公共头部c运行typedef struct _object { Py_ssize_t ob_refcnt; // 引用计数 struct _typeobject *ob_type; // 类型指针 } PyObject;ob_refcnt引用计数决定对象何时被回收ob_type指向类型对象int、str、list 等任何对象哪怕a 1都至少占24 字节64 位系统。这就是 Python 对象比 C 语言变量更占内存的根本原因。5.2 整数对象 int 内存布局Python3 中 int 是变长对象c运行struct _longobject { PyVarObject ob_base; digit ob_digit[1]; };小整数-5~256全局驻留不会重复分配大整数按需分配一个简单 int 对象至少占28~32 字节5.3 字符串对象 str 内存布局字符串是不可变对象且开启intern 机制c运行struct PyASCIIObject { PyVarObject ob_base; Py_ssize_t length; unsigned int state; char data[]; };相同字符串内存共享长度 引用 缓存 字符数据内存紧凑但有固定头部开销5.4 列表 list 内存布局list 本质是动态数组c运行typedef struct { PyObject_VAR_HEAD PyObject **ob_item; // 指针数组 Py_ssize_t allocated; // 已分配容量 } PyListObject;ob_item指向存储指针的连续内存allocated是总容量大于等于实际长度append 时不够容量会扩容new old (old 1) 1列表扩容机制是导致内存突然上涨的常见原因。5.5 字典 dict 内存布局现代 Python3.7dict 是紧凑哈希表c运行struct PyDictObject { PyObject_HEAD Py_ssize_t ma_used; PyDictKeysObject *ma_keys; PyObject *ma_values; };字典有负载因子到达阈值自动扩容与重哈希这也是 dict 比较占内存的原因。六、Python 内存分配完整生命周期一个对象从创建到销毁内存经历了什么6.1 对象创建流程执行代码a MyObj()计算对象所需内存大小≤512 字节 → PyMalloc 分配 Block512 字节 → libc malloc /mmap初始化对象头部引用计数、类型执行__init__返回对象指针6.2 内存复用机制Python 为了性能大量使用内存复用小整数池-5~256 全局唯一不重复分配。字符串驻留 intern相同字符串只存一份。free list 复用释放的对象不销毁而是挂到 free list下次直接复用。例如list、dict、tuple 都有 free list。6.3 内存释放流程引用计数减为 0执行对象析构逻辑如有内存归还 PyMalloc Pool 或 free list不主动归还操作系统只有空闲 Arena 过多时才会部分归还这解释了一个经典现象Python 程序内存上去了就很难降下来。七、垃圾回收与内存分配的关系内存分配和回收是一体两面必须一起讲。7.1 引用计数主回收机制每个对象维护ob_refcnt赋值、传参、入容器都会 1离开作用域、del、替换引用都会 -1计数为 0 → 立即释放引用计数是实时内存回收也是 Python 内存分配能高效稳定的基石。7.2 循环引用问题引用计数无法处理python运行a [] b [] a.append(b) b.append(a) del a, b互相引用计数永远 ≥1无法释放。7.3 分代垃圾回收辅助机制Python 使用标记 - 清除分代回收0/1/2 代来处理循环引用。GC 工作时遍历所有 GC 跟踪对象标记存活对象回收不可达对象合并内存、归还内存池GC 不会直接释放内存给 OS只会还给 Python 内存池。八、内存分配相关的常见问题深度解析8.1 为什么 Python 小对象特别占内存举例python运行lst [1, 2, 3, 4, 5]每个 int 至少 28 字节列表本身头部 40 字节存储 5 个指针 40 字节总共约200 字节只存 5 个数字。原因一切皆对象固定头部开销大PyMalloc 8 字节对齐指针数组额外开销8.2 为什么内存只升不降因为PyMalloc 缓存内存池FreeList 缓存对象除非极端空闲否则不还给 OS全局缓存、驻留对象长期存在这不是泄漏是 Python 内存策略。8.3 为什么千万数据跑起来 OOM列表自动扩容倍数增长中间临时对象大量产生内存池不释放数据全部驻留内存解决方案分块、迭代器、生成器、按需加载。8.4 为什么同样代码 Linux 和 Windows 内存占用不同因为不同系统 malloc 实现不同mmap 策略不同内存页回收机制不同PyMalloc 行为一致但底层 OS 表现不同九、从内存分配原理看 Python 内存优化理解原理后优化就非常简单。9.1 减少小对象创建复用对象避免循环内大量新建使用局部变量减少属性查找避免不必要的中间列表9.2 使用更省内存的数据结构array.array代替 list 存数字NamedTuple代替小对象dataclass(slotsTrue)大幅减内存生成器()代替列表[]9.3 合理利用内存池避免频繁创建超 512 字节大对象批量操作减少分配次数长生命周期对象集中管理9.4 避免隐性内存暴涨慎用无界全局缓存列表 / 字典避免无限 append循环引用及时断开大文件分块读取9.5 生产环境实用技巧监控 RSS、虚拟内存、对象数量使用 tracemalloc 定位分配热点用 objgraph 看增长类型必要时手动触发 gc 并清理缓存十、实战脚本观察 Python 内存分配行为python运行import os import gc import psutil def get_rss(): return psutil.Process(os.getpid()).memory_info().rss // 1024 // 1024 def test_memory_allocate(): print(初始内存, get_rss(), MB) # 大量小对象分配 lst [] for i in range(1000000): lst.append(i) print(创建 100w 整数后, get_rss(), MB) # 删除引用 del lst gc.collect() print(GC 后内存, get_rss(), MB) if __name__ __main__: test_memory_allocate()运行结果你会发现GC 后内存并不会明显下降这就是 PyMalloc 内存池保留了内存。十一、总结Python 内存分配不是黑魔法而是一套极其精巧的分层架构虚拟内存 → Arena → Pool → Block → PyObject。核心要点回顾Python 不直接使用物理内存使用虚拟内存 页映射PyMalloc 三层架构Arena/Pool/Block专为小对象优化小对象走内存池大对象走系统 malloc一切皆对象头部开销巨大引用计数负责实时回收GC 处理循环引用内存释放优先还给 Python 池不还给 OS内存优化的本质是减少对象、减少碎片、合理复用理解这些原理你以后遇到内存上涨OOM 崩溃内存泄漏性能缓慢都能从底层定位根源而不是盲目猜测。你在项目中遇到过哪些诡异的内存问题是内存暴涨、OOM、还是 GC 异常欢迎在评论区留言我会一一解答。原创不易欢迎点赞、收藏、关注下期带来《Python GIL 底层实现与高并发突破实战》。