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。

循环引用

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

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

复制代码

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

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

"""

错误!未定义 p, dog

print(p)

print(dog)

"""

复制代码

垃圾回收机制

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

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

收集所有 容器对象 ( list , dict , tuple , customClass, ... ) ,通过一个双向链表进行引用;

针对每一个容器对象,通过一个变量 gc_refs 来记录当前对应的引用计数;

对于每个容器对象,找到它所引用的容器对象,并将这个容器对象的引用计数 -1;

经过步骤 3 后,如果一个容器对象的引用计数为 0,就代表这个对象可以被回收了,肯定是 "循环引用" 才导致它活到现在的。

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

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

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

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

默认一个对象被创建出来后,属于第 0 代;

如果经历过这一代 垃圾回收 后,依然存活,则划分到下一代;

垃圾回收的周期顺序

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

1 代 "垃圾回收" 一定次数后,触发 0~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 p 、del 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,p 、dog 得以被释放。

注意:是 p 、dog 先被释放,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 这条语句中,前面的 p 、p.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

复制代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值