Python内存管理机制
- 1. 堆
- 2. 栈
- 3. 引用
- 4. Python中可变对象和不可变对象
- 有个问题:你可以好好思考下
- 总结
Python内存管理程序是用 C/C++写的,这里我们以 CPython解释器为例说明。
在Python 中 所有数据类型 包括:int dict str都是一个对象,叫做 PyObject 。
下面,我主要从 堆栈 ,引用这三个方面来浅析 CPython 内存管理机制
1. 堆
主要负责存储 CPython运行时的所有对象实体 (也就是 Python对象的所有属性数据),例如:
smt = 'Hello, world ’ 是一个 PyASCIIObject
n = 23 是一个 整数 PyLongObject
- 上面两个例子,他们都是 Python对象。
- 赋值符号 “=” 右边的是 数据值,CPython 会将其存储到 堆内存中。
2. 栈
在CPython的语义中,又叫数据栈或值栈,它主要负责保存对堆中 Python对象的引用。
smt = ‘Hello, World’ , CPython会将 ‘Hello,World’ 这个字符串实体所处的内存地址压入栈中, 而不是将 ‘Hello,world’ 实体压入栈中。
smt 仅仅 持有对该Python对象实体的引用 。
3. 引用
我们先看这个示例
s1 = 'hello world'
s2 = 'hello world'
print(id(s1)) # 2028548222256
print(id(s2)) # 2028548222256
print(s1==s2) # True
- 示例中表明:变量s1和s2 持有Python 对象在堆内存相同的地址。这个可以从打印出的 引用的内存地址和 Python 判断两个对象是否相等(==) 都可以看出来。
- 现在我们来,分析下上面引用地址相同原因
💚💚💚 - 变量 s1持有Python对象 ‘Hello world’ 引用,对于CPython虚拟机来说:在 执行 s1 = ‘Hello world’ 时,将内存地址 0x71334 压入内存栈。
- 当 CPython碰到同样的 语句 s2 = ‘Hello world’ ,由于指向同一个PythonObject对象,那么 s2 和 s1一样,自然也会持有 ‘Hello world’引用,即 s2 实质上也拥有 ‘Hello world’ 堆中的地址。
4. Python中可变对象和不可变对象
我们已 列表对象作为研究主题
L = [11,12,32,45]
print("1.打印列表中的元素所在地址")
for X in L:
print(id(X))
print("2.打印列表对象L的内存地址:",id(L))
print("3.修改第三个元素: 734")
L[2] = 734
print("4.再次打印列表中元素的内存地址")
for X in L:
print(id(X))
print("5.打印列表对象L的内存地址: ",id(L))
# 打印运行结果
1.打印列表中的元素所在地址
2655576719920
2655576719952
*2655576720592*
2655576721008
2.打印列表对象L的内存地址: 2656002024640
3.修改第三个元素
4.再次打印列表中元素的内存地址
2655576719920
2655576719952
*2655577763760*
2655576721008
5.打印列表对象L的内存地址: 2656002024640
根据上述代码和运行结果可知如下事实:
- list 类型的 L 本身 是一个 Python对象,其对象实体就是在 堆内存中。
- list 类型的对象,作为一个容器级别的对象,其列表存储的是元素实体 的引用,而非 元素实体本身。
- 对 list 对象中的某个元素修改的本质 是 :让被修改元素指向其他元素的引用。修改该元素时,实际上CPython 在堆内存中创建一个新的对象来分配新的内存空间,并且保存该新增的对象(整数734),所以 list对象的第三个元素 L(2) 不再保存对 整数32 引用,更新为对 734 的引用。
- 但是list类型对象在其元素修改前后,变量L对象 始终引用同一个 list对象。
从上面的例子我们也可以知道
- 可变对象:其内部元素可修改是指:变更对其他 Python对象的引用(如:列表中第二个元素 L[2] ),可变对象的元素 可以是:数字,字符串,甚至是其他容器级别的可变对象
- 不可变对象:上面示例中list 的元素都是不可变对象,Python中原始数据类型,如:数字类型(int, float) 字符串(str) 字节数字(bytes)
有个问题:你可以好好思考下
我们知道,整数类型是右值,理论上应该返回具体值,但是为什么能返回内存地址了 ?
💚💚💚
回答这个问题之前,我们需要知道:在Python中,一切事物都是对象,不论是整数,字符串,甚至是其他容器级别的数据类型,都是由CPython的C 底层由一个叫 struct PyObject 结构体所封装。
- PyObject的结构体在 CPython运行时存储在堆中,对于C底层来说,任意的PyObject结构体 返回的是内存地址,因此它是一个左值
- 但是对于 Python语义来说,不存在 静态语言汇总的左值和右值,它只能理解的是 PyObject 这个C实现的对象。
总结
我们理解了CPython的基本内存模型后,但要说的是,这是一个简化的内存模型, CPython虚拟机对于堆内存管理有一套较为复杂的内存池管理方案。通过上面的分析,我们知道了 Python内存管理的两个基本概念
- 什么是 CPython的栈引用
- 什么是 CPython对象的引用
我们从堆内存的角度理解为什么 CPython要对 Python对象分类 可变对象和不可变对象,这是因为 Python 变量持有 Python对象的引用(从C的角度就是:Python变量持有PyObject对象的指针)去访问 Python 对象实体本身,比持有 Python 对象实体的副本 更高效,更节省堆和栈内存开销 - 当 多个Python变量 引用同一个 Python对象就 会涉及到 引用计数器,引用计数器属于内存垃圾回收的范畴,由引用计数又会 牵扯出 CPython一个致命 的诟病,GIL(全局解释器锁),为什么这么多年CPython不能去掉 GIL ,很大原因就是引用计数器有关。