在前面的内容中我们已经知道,id() 返回对象的内存地址,那一个Python对象,在内存中到底占用多少字节?这些字节又存储了什么内容?
先做一个简单的测试,看看整数对象的内存大小:
import sys# 测试小整数的内存大小print(f"{sys.getsizeof(1)=}") # 输出 28!也就是说,一个整数1,在内存中占用28字节
再测试一下小整数的内存地址间隔:
for i in range(5): print(f"id({i+1}) - id({i}) = {id(i+1) - id(i)}") # 输出结果都是32
这里就有两个疑问:为什么一个整数占用28字节,而内存地址的间隔却是32字节?答案很简单:id() 返回的是对象的“起始内存地址”,而Python在分配内存时,会进行内存对齐(通常是8字节对齐),确保内存块的长度是8的整数倍,提升内存访问效率。
结合CPython的官方文档,我们可以拆解出一个整数对象(小整数)的28字节内存结构(剩余4字节用于内存对齐,凑够32字节):
8字节:引用计数(对于不朽对象,这里存储的是 0xc0000000 这样的标志);8字节:类型指针(指向该对象的类型,比如整数对象的类型是 int);8字节:大小字段(Python的整数可以是任意长度,这个字段用来记录整数的位数);4字节:实际的数字(如果是大整数,会额外分配内存存储更多数字);4字节:内存对齐(凑够32字节,满足8字节对齐要求)。
在CPython的C语言底层,所有Python对象都继承自一个基础结构体——PyObject,它是Python对象的“基类”,定义了所有对象都必须具备的两个核心字段:
typedef struct _object { Py_ssize_t ob_refcnt; // 引用计数(或不朽对象标志) PyTypeObject *ob_type; // 类型指针,指向对象的类型} PyObject;
而整数对象(在C底层叫做“长整数”),则是在 PyObject 的基础上,扩展了自己的字段:
// 整数对象的数值部分typedef struct _PyLongValue { uintptr_t lv_tag; /* 数字的位数、符号和标志 */ digit ob_digit[1]; /* 整数的实际数值,是一个32位整数数组 */} _PyLongValue;// 整数对象的完整结构struct _longobject { /* C层面,Python的int叫做long integer */ PyObject_HEAD /* 继承PyObject的两个字段 */ _PyLongValue long_value; /* 整数的数值部分 */};
其中,PyObject_HEAD 就是对 PyObject 结构体的引用,是Python内存管理器用来检索、回收对象的核心依据。
了解这些底层结构,不需要我们会写C代码,只要能明白:Python的每一个对象,都有一个基础的“头部”(存储引用计数和类型),再加上自己的“数据部分”(存储实际值),这就够了。
看到这里,大家可能会觉得:引用计数已经能完美管理内存了——引用为0就回收,简单又高效。但实际上,引用计数有一个致命的“短板”:无法处理循环引用。比如两个对象,互相引用对方,即使没有其他任何引用指向它们,它们的引用计数也永远不会降到0,内存就会被一直占用,导致内存泄漏。
说到循环引用,那是我们下次要看的内容。