Python 内存管理机制

3,385 阅读9分钟

本文根据 撩课-Python内存管理机制 整理而成。

引用计数器机制

当一个对象被引用时,引用计数 +1,当这个对象不再被引用,或引用它的对象被释放时,引用计数 -1,当对象的引用计数为 0 时,释放该对象。

使用 sys.getrefcount(obj) 可以查看一个对象的当前引用计数。在 Python 中,当对象被传入到一个函数时,在这个函数的内部有会两个对象引用着它。但是 sys.getrefcount(obj) 比较特殊,通常只引用一次。

class Person:
    pass

def log(obj):
    # obj += 2
    print(sys.getrefcount(obj))  # obj += 1

p = Person()  # p = 1
log(p)  # p = 4

print(sys.getrefcount(obj))  # p = 2

对象在离开函数作用域时,会断开和函数对象之间的引用,因此最后 p 的引用计数为 2。

循环引用

简单来说,当一个对象不再使用时,应该被释放,但是,当对象被删除后仍然存在引用计数时,将无法释放该对象。

class Person:
    def __del__(self):
        print("Person({0}) 被释放".format(id(self)))

class Dog:
    def __del__(self):
        print("Dog({0}) 被释放".format(id(self)))

p = Person()  # p = 1
dog = Dog()  # dog = 1

# 循环引用
p.pet = dog  # dog = 2
dog.master = p  # p = 2

# 程序结束前 __del__() 不被调用
# 由于循环引用,本质上无法真正删除 p, dog,只是在语法层面上删除了它们。
del p, dog  # p, dog = 1, 1

在语法层面上,pdog 被删除后就无法再使用了,也无法通过 pdog 的属性 petmaster 来找到它们。 因此,将 pdog 称之为 可到达的引用,将 petmaster 称为 不可到达的引用。也就是说,将 pdog 删除后,虽然 petmaster 所引用的 dogp 还在内存中,但是已经无法通过正常手段来访问他们了,pdog 对象将在内存中无法被释放掉。

当被 del 后的对象还存在引用计数时,通过 引用计数器机制 就无法做到真正从内存中回收它们,于是就造成了,由循环引用引起的内存泄漏问题。

"""
错误!未定义 p, dog
print(p)
print(dog)
"""

垃圾回收机制

Python 由两套内存管理机制并存,分别是 引用计数器机制垃圾回收机制。引用计数器机制性能优于垃圾回收机制,但是无法回收循环引用。因此,垃圾回收机制的主要作用在于,从 经历过引用计数器机制后 仍未被释放的对象中,找到循环引用并释放掉相关对象。

垃圾回收的底层机制(如何找到循环引用?)

  1. 收集所有 容器对象 ( list , dict , tuple , customClass, ... ) ,通过一个双向链表进行引用;
  2. 针对每一个容器对象,通过一个变量 gc_refs 来记录当前对应的引用计数;
  3. 对于每个容器对象,找到它所引用的容器对象,并将这个容器对象的引用计数 -1;
  4. 经过步骤 3 后,如果一个容器对象的引用计数为 0,就代表这个对象可以被回收了,肯定是 "循环引用" 才导致它活到现在的。

分代回收(如何提升查找循环引用的性能?)

如果程序中创建了很多个对象,而针对每一个对象都要参与 检测 过程,则会非常的耗费性能,基于这个问题,Python 提出了一个假设,那就是:越命大的对象越长寿。

假设一个对象被检测 10 次都没有把它释放掉,就认定它一定很长寿,就减少对这个对象的 检测频率

分代检测(基于假设设计出的一套检测机制)

  1. 默认一个对象被创建出来后,属于第 0 代;
  2. 如果经历过这一代 垃圾回收 后,依然存活,则划分到下一代;

垃圾回收的周期顺序

  • 0 代 "垃圾回收" 一定次数后,触发 0~1 代回收;
  • 1 代 "垃圾回收" 一定次数后,触发 0~2 代回收。

关于分代回收机制,它主要的作用是可以减少垃圾检测的频率。严格来说,除了它有这个机制限定外,还有一个限定它的条件,那就是,在 垃圾回收器 中,当 "新增的对象个数 - 销毁的对象个数 = 规定阈值" 时才会去检测。

触发垃圾回收

  1. 自动回收

    触发条件是,开启垃圾回收机制 ( 默认开启 ),并且达到了垃圾回收的阈值。

    需要注意的是,触发并不是检查所有的对象,而是分代回收。

  2. 手动回收 ( 默认0~2 )

    只需执行 gc.collect(n)n 可以是 0~2,表示回收 0~n 代垃圾。

gc 模块

gc 模块可以查看或修改 垃圾回收器 当中的一些信息。

import gc
  • gc.isenabled()

    判断垃圾回收器机制是否开启。

  • gc.enable()

    开启垃圾回收器机制 ( 默认开启 ) 。

  • gc.disable()

    关闭垃圾回收器机制。

  • gc.get_threshold()

    获取触发执行垃圾检测阈值,返回值是一个元组 ( threshold, n1, n2 )

    • threshold

      就是触执行发垃圾检测的阈值,当 新增的对象个数 - 销毁的对象个数 = threshold 时,执行一次垃圾检测。

    • n1

      表示当 0 代垃圾检测达到 n1 次时,触发 0~1 代垃圾回收。

    • n2

      表示当 1 代垃圾检测达到 n2 次时,触发 1~2 代垃圾回收。

  • gc.set_threshold(1000, 15, 15)

    修改垃圾检测频率。一般情况下,为了程序性能,会把这些数值调大。

测试自动回收 1

import gc

# "创建对象的次数 - 销毁对象的次数 = 2" 时,触发自动回收。
gc.set_threshold(2, 10, 10)

class Person:
    def __del__(self):
        print(self, "被释放")

class Dog:
    def __del__(self):
        print(self, "被释放")

p = Person()  # p = 1
dog = Dog()  # dog = 1

# 循环引用
p.pet = dog  # dog = 2
dog.master = p  # p = 2

# 多创建一个 Person 类,目的是为测试在删除对象后,程序能够触发自动回收。
p2 = Person()

# 程序结束前,不调用 __del__()。
del p
del dog

总共创建 3 个对象,销毁了 1 个对象,3-1=2。理论上说,此时应该触发自动回收,但直到程序结束之前,__del__() 函数都没有被调用,这是为什么呢?

要解释这个问题,首先就要了解,为什么垃圾检测会存在 "新增的对象个数 - 销毁的对象个数 = 规定阈值" 这样一个限定条件。

这是因为,当对象遗留在内存中无法被释放时,原因通常是对象创建多了而没有被及时销毁的原因。

那么根据这个结论,就可以设定一个机制,当 "创建的对象" 多出 "被销毁的对象" 大于或等于 "指定阈值" 时,再让程序去检测垃圾回收,否则不触发检测。

在销毁一个对象时,表现的是,将减少一次达到指定阈值的条件,也就没有必要再去检测了。

所以严格来说,这个限定条件要改成:在创建对象时,"新增的对象个数 - 销毁的对象个数 = 规定阈值" 时 ,触发垃圾检测。

了解了这些之后,你就知道,为什么这里对象无法被释放了。首先创建了 3 个对象,然后执行 del pdel dog,而在执行销毁操作时,是不会触发垃圾检测的,因此对象不被释放。

注意

此结论是我个人推测的,也有可能真是情况并不是这样。我也是想了好久为什么不释放对象,最终想到的一个比较合理的解释。

测试自动回收 2

import gc
gc.set_threshold(2, 10, 10)

class Person:
    def __del__(self):
        print(self, "被释放")

class Dog:
    def __del__(self):
        print(self, "被释放")

p = Person()  # p = 1
dog = Dog()  # dog = 1

# 循环引用
p.pet = dog  # dog = 2
dog.master = p  # p = 2

# 尝试在删除 "可到达引用" 后,真实对象是否有被回收。
del p, dog

# 多创建一个 Person 类,目的是为测试在删除对象后,程序能够触发自动回收。
p2 = Person()
print("p2 =", p2)

print("----------------------- 程序结束 -----------------------")

"""
<__main__.Person object at 0x0000000002c28190> 被释放
<__main__.Dog object at 0x0000000002cf33d0> 被释放
p2 = <__main__.Person object at 0x0000000002cf3350>
----------------------- 程序结束 -----------------------
<__main__.Person object at 0x0000000002cf3350> 被释放
"""

总共创建 5 个对象,销毁了 3 个对象,5-3=2,触发自动检测。此时发现 p , g 已被销毁 ( 真实对象还在内存中 ),于是找到它们所引用的对象,将计数 -1,pdog 得以被释放。

注意:是 pdog 先被释放,p2 在程序结束后被释放。

手动回收

import gc

class Person:
    def __del__(self):
        print(self, "被释放")

class Dog:
    def __del__(self):
        print(self, "被释放")

p = Person()  # p = 1
dog = Dog()  # dog = 1

# 循环引用
p.pet = dog  # dog = 2
dog.master = p  # p = 2

del p  # p = 1
del dog  # dog = 1

# 对程序执行垃圾检测 (无关回收机制是否开启),手动回收内存。
gc.collect()

# <__main__.Person object at 0x109cb0110> 被释放
# <__main__.Dog object at 0x109cb0190> 被释放

弱引用

import weakref
import sys

class Person:
    def __del__(self):
        print(self, "被释放")

class Dog:
    def __del__(self):
        print(self, "被释放")

p = Person()  # p = 1
dog = Dog()  # dog = 1

p.pet = dog  # dog = 2
# weakref.ref 不强引用指定对象 (即不增加引用计数)。
dog.master = weakref.ref(p)  # p = 1

# p 被完全销毁时,它所引用对象的计数 -1.
del p  # p = 0, dog = 1
del dog  # dog = 0

# <__main__.Person object at 0x109cb0110> 被释放
# <__main__.Dog object at 0x109cb0190> 被释放

为证明一个对象被销毁时,它所引用对象的计数是否 -1,特此做个实验,来观察 p 被销毁时,它所指向的 dog 引用计数。

p.pet = dog  # dog = 2
dog.master = weakref.ref(p)  # p = 1

del p  # p = 0, dog = 1

"""
观察 p 被销毁时,它所引用的 dog 计数是否被 -1
sys.getrefcount 用于获取一个对象的当前引用计数,返回值比实际值多 1。
"""
print(sys.getrefcount(dog))  # 2

del dog  # dog = 0

p 被销毁时,意味着在 p.pet = god 这条语句中,前面的 pp.pet 已经不存在了,只剩下 = dog ,前面空空如也,并不被任何对象所引用,因此 dog 的引用计数 -1。

而在强引用下,p 被销毁时,dog 的引用计数不变。

p.pet = dog  # dog = 2
dog.master = p  # p = 2

del p  # p = 1, dog = 2
print(sys.getrefcount(dog))  # 3,实际值为 2.
del dog  # dog = 1

要在一个集合中弱引用对象,使用 weakref.Weak...

# 弱所引用字典中的对象
# pets = weakref.WeakValueDictionary({"dog": d1, "cat": c1})

手动打破循环引用

class Person:
    def __del__(self):
        print(self, "被释放")

class Dog:
    def __del__(self):
        print(self, "被释放")

p = Person()  # p = 1
dog = Dog()  # dog = 1

p.pet = dog  # dog = 2
dog.master = p  # p = 2

"""
在删除前手动打破循环引用

这意味着手动断开 p.pet 与 dog 之间的引用,
当 dog 不再被 p 引用时,计数自然 -1。
"""
p.pet = None
del p  # p = 0, dog = 1
del dog  # dog = 0