我们知道在Python编程中,即便较为权威《Python编程指南》一书,也并没有要求Python读者去掌握系统性地理解CPython内部实现中的内存分配以及内存回收等知识。甚至泛滥于网络上的Python编程技术文章很少系统完整性地谈及Python的内存管理。但是知道CPython的内存管理原理,有助于我们编写更高效的代码,有助于我们对较慢的Python代码进行故障排除。
Python的实现版本有很多,例如Jython底层就是JVM,IronPython的底层是.Net,它们的内存管理千差万别取决于底层的运行时系统。我这里说的CPython实现,当然读者若有C/C++的基础,理解CPython的内存管理机制有莫大的帮助。
在掌握CPython如何管理Python对象并将齐存储到内存中,我假定你对堆和栈有一个基本的了解。在CPython实现中,堆和栈有各自的职责
- 堆:主要负责存储CPython运行时的所有对象实体(也就是Python对象的所有属性数据),例如smt='Hello Word'这个字符串对象,n=23这是一个整数,它们都是Python对象,赋值符号“=”右边的数据值,CPython会将其存储到堆内存中。
- 栈:在CPython的语义中,又叫数据栈或值栈,它主要负责保存对堆中Python对象的引用,例如当CPython在执行smt='Hello Word'这个简单的Python语句,CPython会将'Hello Word'这个字符串实体所处的内存地址压入栈(对于Python语义级别理解,就是对"Hello Word"的引用),而不是将'Hello Word'这个字符串值压入栈。
从上面对smt='Hello Word'这些简单的Python赋值语句,你不能单纯地认为将‘Hello World’赋值给变量smt,这是大错特错的,由上面的CPython的简易内存模型可知。,赋值符号右边的是Python对象实体(从C实现的理解,就是构成该PyObject对象所有属性值,这些值有具体的字面量值表示),并且CPython会为该Python对象在堆中分配内存并且存储它。而变量smt仅持有该Python对象实体的引用(从C实现的理解,就是该PyObject对象的内存地址),而不是实际的Python对象.
如果你对C/C++的指针有所理解的话,应该头脑清晰的,其实上面说那么多就是要导出一个概念,什么是Python对象的引用。再来看一个示例。
我们s1变量持有Python对象‘Hello’的引用,对于CPython虚拟机来说,就是在执行s1='Hello Word',将它的内存地址0x71334推入数据栈,那么当CPython碰到同样的语句s2='Hello Word',明显是指向同一个Python对象,那么变量s2和s1一样,它自然持有是‘Hello Word’的引用,即s2实质上拥有的‘Hello Word’的堆中的地址。
ss8.png
对于其他简易的数据类型,也是如出一辙的。那么现在给Python引用我们可以下一个定义。
Python对象的引用:就是Python变量持有Python对象在堆内存中的内存地址。我们可以通过python的内置id函数来正式我们刚才的分析
或者可以使用关键字is 来判断两个变量是否对同一个对象的引用。
在Python中有两种类型的对象,就是可变对象和不可变对象。
可变对象:这个很好例子,比较典型的就是list,一个列表作为一个对象存储在堆内存中,如果我们要更改该列表的某些元素,它将仍然是内存中的同一个列表对象。我们来看看下面一段非常无聊的Python代码
我们在修改列表L中,我们通过列表表达式打印出列表每个对象元素的内存地址,以及列表对象L本身的内存地址,然后在修改列表元素后,再次打印列表对象中的各个对象元素的内存地址,以及L本身的内存地址。
这段代码告诉我们CPython在内存中有如下事实
- list类型的L本身是一个Python对象,其对象实体就是在堆内存中。
- list类型的对象,作为一个容器级别的对象,其列表存储的是元素实体的引用,而非元素实体本身。
- 对list对象中的某个元素的修改的本质是令被修改元素指向其他元素的引用,而我们修改该元素时,实际上CPython在堆内存中创建了一个新的对象(本例中的整数734)分配新的内存空间,并且保存该新增的对象(整数734)。L的第三个元素不再对32的引用,更新为对734的引用
-
list类型对象的在其元素修改前后,变量L始终引用同一个lsit对象。
那么从上面的例子,我们可以用一个内存图来表示list对象前后的变化,并且我们得知
- 可变对象的实质:其内部元素可修改是可变更对其他Python对象的引用。其可变对象的元素可以是数字、字符串,甚至可以是其他容器级别的可变对象。
- 不可变对象就非常容易理解了,上面示例中list的元素对象都是不可变对象。推而广之,Python中的原始数据类型,例如数字类型(int,float)、字符串(str)、字节数组(bytes)。
题外话:整数类型不是右值吗?为什么能返回内存地址?
在Python中,一切事物都是对象,不论是整数,字符串,甚至是其他容器级别的数据类型,都由CPython的C底层由一个叫struct PyObject结构体所封装。PyObject的结构体在CPython运行时存储在堆内中,对于C底层来说,任意的PyObject结构体能够返回内存地址因此是一个左值,但对于Python语义来说,不存在静态语言中的左值和右值,它只能理解的是PyObject这个C实现的对象。
小结
我们理解了CPython的基本的内存模型后,但要说的是,这是一个简化的内存模型,CPython虚拟机对于堆内存管理有一套较为复杂的内存池管理方案。我们后面章节会逐一谈到。目前,我们至少知道了两个基本的概念
- 什么CPython的栈和堆
- 什么是Python对象的引用
我们从堆内存的角度理解为什么CPython要堆Python对象分类可变对象和不可变对象,初衷是尽可能低简化堆内存的分配,因为Python变量持有Python对象的引用(或者从C底层去理解,持有PyObject对象的指针)去访问Python对象实体本身,比持有一个Python对象实体的副本更高效,更节省堆和栈的内存开销。
那么当多个Python变量引用同一个Python对象就涉及到概念就引用计数器,引用计数器属于内存垃圾回收的范畴,由引用计数又会牵涉到CPython一个致命的诟病,GIL:全局解释器锁,为什么多年来CPython不能去掉GIL,很大原因跟引用计数器有关。我们后面文章会谈到这些。
网友评论