6. Python的内存管理

建议:完成基础语法的学习后再看

内存是每一个程序都要打交道的地方,不同于C类语言的手工管理,Python是一门动态的面向对象的解释型语言,对象与引用是分离的。内存也是由解释器动态管理的。 Python中对于对象的赋值操作就是一个创建对象与引用的过程。在第一章我们提到的赋值语句a=1就是将1这个对象与a引用了起来。

查看内存地址

Python中有一个内置函数id(),就是用来返回一个对象的内存地址的。而利用hex(),我们就能看到实际的十六位内存地址。 例如:

a = 1

print(id(a))
print(hex(id(a)))

引用计数

在Python中,一切皆对象,为了提升效率,在一定情况下,一个对象会被引用多次。 引用计数就是计算一个对象被引用了多少次的一个量化的表示。

引用计数的增加

  • 赋值语句

  • 实参传参

    引用计数的减少

  • 函数运行结束

  • 当用del()删除某个引用时

  • 如果某个引用指向对象A,当这个引用被重新定向到某个其他对象B时,对象A的引用计数减少。

    引用计数的查看

    我们可以使用sys模块中的getrefcount(),来查看某个对象的引用计数。

    需要注意的是,当使用某个引用作为参数,传递给getrefcount()时,参数实际上创建了一个临时的引用。因此,getrefcount()所得到的结果,会比期望的多1。

    特殊点

    -5到256的这些数字,Python中默认会创建对象来优化,所以会提前缓存好,在使用过程中直接引用就可以了。但是超过这个范围,只有在同一代码块中出现才会有相同的内存地址。短字符串也会在创建之后会被引用多次。

    a = 0
    b = 0
    id(a) == id(b)

    True

    a = -5
    b = -5
    id(a) == id(b)

    True

    a = -6
    b = -6
    id(a) == id(b)

    False

    a = 256
    b = 256
    id(a) == id(b)

    True

    a = 257
    b = 257
    id(a) == id(b)

    False

    a = -1.0
    b = -1.0
    id(a) == id(b)

    False

对象间的引用

Python的一个容器对象,比如列表、词典等,可以包含多个对象。实际上,容器对象中包含的并不是元素对象本身,是指向各个元素对象的引用。列表中如果引用列表,可能会有意想不到的问题。参见copy模块​。 容器对象的引用可能构成很复杂的拓扑结构,因此在刚开始的阶段不建议进行容器对象的引用。 两个对象可能相互引用,从而构成所谓的引用环(reference cycle)。

a = []
b = [a]
a.append(b)

即使是一个对象,只需要自己引用自己,也能构成引用环。

a = []
a.append(a)
print(sys.getrefcount(a))

引用环会给垃圾回收机制带来很大的麻烦。

垃圾回收

Python中,拥有一种管理内存的机制,就是垃圾回收。从基本原理上,当Python的某个对象的引用计数降为0时,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。比如某个新建对象,它被分配给某个引用,对象的引用计数变为1。如果引用被删除,对象的引用计数为0,那么该对象就可以被垃圾回收。这时候我们称之为标记清除。 但是一个对象的引用计数减为零之后不会被立即清理掉,Python只会在特定条件下启动垃圾回收。当Python运行时,会记录其中分配对象(object allocation)和取消分配对象(object deallocation)的次数。当两者的差值高于某个阈值时,垃圾回收才会启动。

我们可以通过gc模块的get_threshold()方法,查看该阈值:

import gc
print(gc.get_threshold())

当然,我们也可以手动启动垃圾回收,即使用gc.collect()来进行垃圾回收。但由于频繁的垃圾回收具有效率问题,因此需要考虑是否需要进行后再操作。

分代回收

Python将所有的对象分为0,1,2三代。所有的新建对象都是0代对象。当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象。垃圾回收启动时,一定会扫描所有的0代对象。如果0代经过一定次数垃圾回收,那么就启动对0代和1代的扫描清理。当1代也经历了一定次数的垃圾回收后,那么会启动对0,1,2,即对所有对象进行扫描。

参考

Last updated