当前位置:首页>python>Python内存分配,从一个List对象讲起

Python内存分配,从一个List对象讲起

  • 2026-10-21 13:27:27
Python内存分配,从一个List对象讲起

内存分析工具一览

Python的内存分析工具有一些,举几个流行的:能追踪的标准库tracemalloc,到能递归统计的Pympler(objsize代替),再到能分析C扩展的大杀器Memray。

维度
tracemalloc
Pympler
objsize
Memray
标准库
✅
❌
❌
❌
统计对象结构
❌
✅
⚠️ 简化
❌
递归统计
❌
✅
✅
❌
行号 / traceback
✅
❌
❌
✅
Python 对象
✅
✅
✅
✅
C 扩展内存
❌
❌
❌
✅
malloc / free
❌
❌
❌
✅
生命周期(是否释放)
❌
❌
❌
✅
内存火焰图
❌
❌
❌
✅
工程侵入性
低
中
低
高
运行时开销
低
中
低
高

但这篇主要不是对比工具,而是来分析一个简单的代码,来看看Python的内存分配。

递归分析Python对象

示例代码很简单,就一个列表,分析其内存具体分配了多少。

a = ['1', 2, '1']

递归打印这个list对象内存占用:

a = ['1', 2, '1']def deep_size(obj, seen=None, level=0):    if seen is None:        seen = set()    obj_id = id(obj)    if obj_id in seen:        return 0    seen.add(obj_id)    size = sys.getsizeof(obj)    indent = "  " * level    print(f"{indent}{type(obj).__name__} id={hex(obj_id)} size={size}")    if isinstance(obj, dict):        for k, v in obj.items():            size += deep_size(k, seen, level + 1)            size += deep_size(v, seen, level + 1)    elif isinstance(obj, (list, tuple, set, frozenset)):        for item in obj:            size += deep_size(item, seen, level + 1)    elif hasattr(obj, '__dict__'):        size += deep_size(vars(obj), seen, level + 1)    return sizes = deep_size(a)print(f"Total size: {s} bytes")sys.getsizeof(a)print(f"sys.getsizeof: {sys.getsizeof(a)} bytes")

输出如下:

list id=0x1388cbaa780 size=88  str id=0x7ffd651bc6e0 size=42  int id=0x7ffd651ad3c8 size=28Total size: 158 bytesobjsize.get_deep_size: 158 bytessys.getsizeof: 88 bytes或者:list id=0x1388cbaa780 size=72  str id=0x7ffd651bc6e0 size=42  int id=0x7ffd651ad3c8 size=28Total size: 142 bytesobjsize.get_deep_size: 142 bytessys.getsizeof: 72 bytes

上述两个输出,为何 list object本身大小会变化?有时72有时88。

因为:

  • • **list 的大小 = 对象头 + “当前 allocated slot 数 × 指针大小”,**而 allocated ≠ len。
  • • list 并不是“按需分配”的。这是 CPython list 的核心策略: list 会 over-allocate(超额分配)。
  • • 这是为了: append 快,减少 realloc,摊还 O(1)。

另一个细节是,列表中的两个"1”引用同一个对象,因此只算一次内存。

import objgrapha = ['1', 2, '1']objgraph.show_refs([a], filename='sample-graph.pdf')

list object自身为何占这么大内存

在 CPython 源码里(Include/cpython/listobject.h):

typedef struct {    PyObject_VAR_HEAD    PyObject **ob_item;    Py_ssize_t allocated;} PyListObject;

PyObject_VAR_HEAD宏再展开,最后如下:

typedef struct {    Py_ssize_t ob_refcnt;        // PyObject    PyTypeObject *ob_type;       // PyObject    Py_ssize_t ob_size;          // PyVarObject (len)    PyObject **ob_item;          // 指向元素指针数组    Py_ssize_t allocated;        // 容量(slot 数)} PyListObject;

在笔者的 64 位 CPython 中:

字段
来源
大小
ob_refcnt
PyObject
8
ob_type
PyObject
8
ob_size
PyVarObject
8
ob_item
PyListObject
8
allocated
PyListObject
8
合计
40 bytes

但为什么看到的是 72 bytes?

因为:

  • • sys.getsizeof(list)包含 element pointer array
  • • list 初始分配 over-allocate

通常:

list with 3 elements→ allocated = 4→ 4 * 8 = 32 bytes

40+32=72 bytes。

list 的 element pointer array 不在 PyListObject 结构体里, 而是在 ob_item 指向的“另一块单独分配的堆内存”中,

┌──────────────────────────┐│ PyListObject             │  ← 固定大小(40 bytes)│                          ││ ob_refcnt                ││ ob_type                  ││ ob_size  (len)           ││ ob_item  ────────────────┼──┐│ allocated                │  │└──────────────────────────┘  │                               ▼                ┌──────────────────────────┐                │ element pointer array     │                │ (PyObject *)              │                │                            │                │ [0] → PyObject* ('1')     │                │ [1] → PyObject* (2)       │                │ [2] → PyObject* ('1')     │                │ [3] → NULL / garbage      │                │ [4] → ...                 │                │ [5] → ...                 │                └──────────────────────────┘

实际debug中某次具体打印出来值,与上述结构对应:

-exec p *(PyListObject *)op$8 = {  ob_base = {    ob_base = {      ob_refcnt = 1,      ob_type = 0x560f754aeb00 <PyList_Type>    },    ob_size = 2  },  ob_item = 0x785a7af77310,  allocated = 2}

另外两个str、int对象也可类似展开,来查看其内存大小组成,不赘述。

最新文章

随机文章