
2.8 【大牛讲坛】从底层理解内存管理
编程语言的内存管理是语言设计的一个重要方面,它也是决定语言性能的重要因素。无论是C的手动管理还是Java的垃圾回收,都向我们展示了编程语言的重要特征。
Python的内存管理是由私有堆空间管理的,这里可以把私有堆空间理解为一块拥有连续编号的内存,所有的对象和数据结构都存储在私有堆空间中。程序员没有访问这块内存空间的权限,只有解释器才有权限访问。为Python的堆空间分配内存的是Python的内存管理模块,核心接口只会提供一些访问该模块的方法供程序员使用。Python自由的垃圾回收机制能够回收并释放没有被使用的内存,以便于新程序使用。
1.对象的内存使用
赋值语句是编程语言中最常见的功能了。但即使是最简单的赋值语句,其背后所包含的机制也可以很复杂。首先让我们先来回顾一下前面讲过的赋值语句,然后进行剖析。

这里整数1是一个对象,存储在内存空间中。a是一个变量,利用赋值语句,将指向变量a的地址指向1。由于Python是动态类型的语言,对象与变量分离。比较形象的解释是:Python像筷子一样,通过变量来接触和翻动真正的食物——对象。
例2-47 用id函数显示内存地址

整数和比较短小的字符串,Python都会缓存这些对象,以便重复使用,当我们创建多个赋值为1的变量时,实际是让所有的变量地址都指向同一个对象。
例2-48 验证1的地址

对比可见,a和b是指向同一个对象的不同变量。
每个对象都存有指向该对象的变量总数,即引用计数(reference count),sys模块中的getrefcount可验证变量的使用次数。当系统会创建一个临时引用,getrefcount得到结果会比预期的数值多1。
例2-49 验证引用计数

getrefcount()返回的结果分别是2和3。
2.对象引用对象
列表可以包含多个元素或者对象,实际上列表包含的并不是对象本身,而是指向各个元素对象的内存地址引用。
例2-50 列表的地址引用

可以看出a引用了对象b的地址,所以引用对象是Python最基本的构成方式。对列表而言,这种引用可能会构成很复杂的拓扑结构。我们可以用objgrph模块来绘制其引用关系。
例2-51 objgrph模块的安装

用pip命令成功安装objgrph模块后,要使用import关键字来导入此模块。
例2-52 制作拓扑图

如果两个对象相互引用,就有可能构成所谓的引用环,即使是一个对象,只要自己引用自己,也能构成引用环。但这会给垃圾回收机制带来很大的麻烦。
3.引用减少
在一个列表里面删除一个元素的时候,引用对象的引用计数就有可能减少。
例2-53 验证引用计数减少

还有一种可能,如果这个变量指向别的内容时,引用计数也会减少。
4.垃圾回收
当Python中的对象越来越多,占据的内存也会越来越大,在适当的时候会触发垃圾回收机制,将没用的对象清除,在许多语言中都有垃圾回收机制,如Java和Ruby。
理论上来说,当一个对象的引用计数降为0的时候,说明没有任何引用指向对象,这时候该对象就成为需要被清除的垃圾了。比如某个新建对象,分配给某个引用,引用数为1,当引用被删除之后,引用数为0,那么该对象就可以被垃圾回收。然而清理垃圾是个费力的过程,垃圾回收的时候,Python不能进行其他任务,频繁的垃圾回收会大大降低Python的工作效率。如果内存中的对象不多,就没必要启动垃圾回收。所以Python只会在特定的条件下自动启动垃圾回收。当Python运行的时候,会记录其中分配对象和取消分配对象的次数,两者的差值高于某个阈值的时候,垃圾回收才会启动。
我们可以通过gc模块的get_threshold()来查看该阈值。
例2-54 垃圾回收机制

Python作为一种动态类型的语言,其对象和引用分离的机制,与面向过程的编程语言有很大的区别。为了达到有效地释放内存的目的,Python采用了一种相对简单的垃圾回收机制,即引用计数。但是这种机制又带来新的问题——孤立引用环,这部分涉及更复杂的垃圾回收算法,我们了解一下即可,不必深究。Python与其他的编程语言既有共通性,又有自身特别的地方,对该内存管理机制的理解,是提高Python性能的重要一步。