Python 内存管理机制

本文根据 撩课-Python内存管理机制 整理而成。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。测试

循环引用

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

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 后的对象还存在引用计数时,经过 引用计数器机制 就没法作到真正从内存中回收它们,因而就形成了,由循环引用引发的内存泄漏问题。3d

""" 错误!未定义 p, dog print(p) print(dog) """
复制代码

垃圾回收机制

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

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

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

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

若是程序中建立了不少个对象,而针对每个对象都要参与 检测 过程,则会很是的耗费性能,基于这个问题,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
复制代码