Python 内存管理和垃圾回收机制

内存管理机制

可变对象/不可变对象
  • 可变对象:如列表、字典等,本质是不论怎么改变值,他的地址都不会发生改变。
  • 不可变对象:如int、float、bool、str、元组等,当你重新赋值时,在底层他将会修改指向的数据,而不会对原来指向的值进行修改,对原来的值修改的仅时引用次数减一
内存分配原则

python中一切皆对象,在底层中每个对象都是基于结构体实现的,而这些结构体里都有着上下指向、引用计数和数据类型的属性,在初始化时都会给数据的引用计数设置为1,每当多一个指向时,其引用计数就加一,每删除一个他的指向,就引用计数减一,当为0时就会对该数据进行垃圾回收

缓存机制

在python的内存管理当中可能存在一些缓存机制(如:int、float、str、list等都有),即:将某个数据删除时,其可能不会将这个对象完全销毁,而是将对象存放到一个链表中,当又创建同类型的对象时,将会直接赋值给缓存的同类型对象,再通过变量引用指向他,举例:

>>> a = "xxx"
>>> id(a)
2436976108520
>>> del a
# 删除了字符串a
>>> b = "yyy"
# 创建字符串b
>>> id(b)
# 可以发现内存地址时一样的
2436976108520

垃圾回收机制

对象引用次数

一般情况下垃圾回收基于对象引用次数,当初始化时次数为1,被其他对象引用时加1,使用del本质则是将引用次数减一,而当引用次数变成0以后则会自动触发垃圾回收机制将其回收,代码层面上则是会调用该对象的__del__方法,举例:

class D(dict):
    def __init__(self, name):
        self.name = name
        print("对象:{}被创建!".format(self.name))
    def __del__(self):
        print("对象:{}被销毁!".format(self.name))
a = D('a')
a = D('b')
print("程序结束!")

结果:
对象:a被创建!
对象:b被创建!
对象:a被销毁!
程序结束!
对象:b被销毁!

可以看出第一句实例化A时对象被创建,对象的引用计数初始化为1,当第二句执行时,新的A对象被创建,新的对象引用计数为1,而旧的A对象因为a指向了其他数据,所以引用次数减一,此时旧的A对象引用次数变成0,触发销毁机制,从而自动调用了__del__方法。当程序结束时,因为要回收内存,因此新的对象A也自动调用__del__方法。

查看引用次数

可以用sys.getrefcount()方法来查看引用次数,要注意因为将内容传入该方法时引用也会加1,所以我们实际想知道的引用次数应该是输出的结果减一,举例:

>>> a = 1000
>>> sys.getrefcount(a)
# 加上传入方法的a,引用次数为2
2
>>> b = a
>>> sys.getrefcount(a)
# 因为b也引用了a,所以引用次数加1
3
>>> del b
>>> sys.getrefcount(a)
# 删除了b以后引用次数减1
2
标记清除

前面的引用计数能够解决一般情况下的内存回收问题,但是对于循环引用的情况,可能就会无法回收,从而造成内存泄漏的问题,例如下面代码:

class D(dict):
    def __init__(self, name):
        self.name = name
        print("对象:{}被创建!".format(self.name))
    def __del__(self):
        print("对象:{}被销毁!".format(self.name))

a = D('a')
b = D('b')

a['x'] = b
b['x'] = a

a = 1
b = 1
print("程序结束!")

结果:
对象:a被创建!
对象:b被创建!
程序结束!
对象:a被销毁!
对象:b被销毁!

可以看到上面的两个字典类因为互相指向了,所以即使销毁了,引用计数也永远大于0,此时垃圾回收机制也就不起作用了,所以之后即使a和b都指向了其他值,但因为他们原先指向的字典类互相有指向,引用计数不为0,导致他们直到程序结束内存才被回收。此时如果想要回收,那么就需要先收集垃圾,然后再进行回收,Python提供了gc.collect()方法用于手动回收数据,举例:

import gc

class L(list):
    def __del__(self):
        print(self, "end")

a = L([1,2,3])
b = L([1,2,a])
a[-1] = b
del a, b
# 手动回收第0代(后面会介绍分代回收,总共有3代)
print("gc generation 0 nums:", gc.collect(0))
print("end")

# [1, 2, [1, 2, [...]]] end
# [1, 2, [1, 2, [...]]] end
# gc generation 0 nums: 2
# end

可以看到两个互相引用的对象被回收了,而这种手动回收的方式就基于了标记清除来实现:

  1. 首先GC会对所有活动的对象打上标记,即一个个点,然后他们之间的引用通过指向来表明,此时就构成了一个有向图
  2. 然后GC会从根对象出发,沿着有向边遍历整个图,而对于不可达的对象,那么就被视为需要清理的垃圾对象。
分代回收

建立在垃圾清除的基础上,其将对象的活动时间分为3代,新生的对象在0代,如果他们在第0代中能够存活下来,就会被放入1代里,当在1代中也存活了下来,再被放到2代,默认当对象数量减去释放的对象数量(即当前可达的对象数量)超过700时将会对0代对象进行回收处理,当进行了10次0代回收则会触发1代回收,当进行了10次1代回收则会触发2代回收,这些配置可以通过gc.get_threshold()方法获取,并通过gc.set_threshold()自定义,举例:

>>> gc.get_threshold()
(700, 10, 10)
>>> gc.set_threshold(500, 5, 3)
>>> gc.get_threshold()
(500, 5, 3)

这里我们再来对前面循环引用的情况通过分代回收来查看效果,首先由于默认的设置里是需要对象数量减去释放数量超过700时才会触发,而这里我们使用的对象示例较少,所以需要我们调整这个触发的阈值,然后为了更加明显地看出回收的步骤,这里也重写了__new__方法,代码如下:

import gc
gc.set_threshold(2, 10, 10)
# 第一个参数代表,如果设置为0代表禁用,这里设置2,代表第0代超过2个对象时触发垃圾回收
# 后面两个是对第一代和第二代的进行回收,这里只要大于1就行了
# 等于1的话那么会不停触发对1/2代的回收,从而导致对第0代的回收失败
class D(dict):
    def __new__(self, name):
        print("对象:{}被分配!".format(name))
        return dict.__new__(self)
    def __init__(self, name):
        self.name = name
        print("对象:{}被创建!".format(self.name))
    def __del__(self):
        print("对象:{}被销毁!".format(self.name))

print("初始时的垃圾回收计数器:", gc.get_count())
a = D('a')
b = D('b')
print("创建了两个对象时的回收计数器:", gc.get_count())
a['x'] = b
b['x'] = a

a = 1
b = 2
print("修改了两个对象时的垃圾回收计数器:", gc.get_count())
c = D('c')
# 分配空间给C时,可以看到触发了第0代的回收
print("新分配空间给对象C时的垃圾回收计数器:", gc.get_count())
print("程序结束!")

结果:
初始时的垃圾回收计数器: (0, 8, 1)
对象:a被分配!
对象:a被创建!
对象:b被分配!
对象:b被创建!
创建了两个对象时的回收计数器: (2, 8, 1)
修改了两个对象时的垃圾回收计数器: (2, 8, 1)
对象:c被分配!
对象:a被销毁!
对象:b被销毁!
对象:c被创建!
新分配空间给对象C时的垃圾回收计数器: (0, 9, 1)
程序结束!
对象:c被销毁!

可以看出在我们的主要代码跑起前已经进行过8次1代和1次2代的垃圾回收了,当创建了两个对象以后,0代增加了2个,修改了这两个对象的指向后,计数器看起来还是2个a和b,但是实际上因为原来的两个字典循环引用导致未被释放,所以实际有4个,只是有2个是不可达的,因此在给对象c分配空间时计数器增加1变成3,因为超过了2,需要进行一次对0代的垃圾回收,因此a和b这两个不可达的就被销毁,然后再创建对象c,最终程序结束,将未被释放的对象a、b和c都销毁

更多参考:
https://blog.csdn.net/it_yuan/article/details/52850270
https://www.jb51.net/article/79306.htm
https://www.jianshu.com/p/0c37059ce224
https://testerhome.com/topics/16556

弱引用

当引用某个数据时,引用计数不会加一,假如有些数据被删除后,希望直接被垃圾回收,就可以利用弱引用来实现,举例:

import weakref

s = {1,2,3}
w = weakref.ref(s)
print(w())
s.remove(1)
print(w())
del s
print(w())

# {1, 2, 3}
# {2, 3}
# None
弱引用集合
  • 示例1:
import weakref

class A: pass
class B: pass

s = {1,2,3}
w = weakref.WeakSet()
a = A()
b = B()
w.add(a)
w.add(b)
print(w.data)
del a
print(w.data)

# {<weakref at 0x000002105CAE38B8; to 'A' at 0x000002105C83A2B0>, <weakref at 0x000002105CAE3778; to 'B' at 0x000002105C9494E0>}
# {<weakref at 0x000002105CAE3778; to 'B' at 0x000002105C9494E0>}
  • 示例2:
import weakref

class A:
    def __del__(self):
        print("对象A被删除!")

a = A()
# b是a的引用
b = a
# c是a的弱引用
c = weakref.ref(a)
# 创建一个弱引用集合
s = weakref.WeakSet()
# 往集合当中添加一个对a的弱引用
s.add(a)
print("a的弱引用:", weakref.getweakrefs(a), "数量:", weakref.getweakrefcount(a))
del a
print("c指向的对象:", c())
del b
print("c指向的对象:", c())

# a的弱引用: [<weakref at 0x000001F8C66FB548; to 'A' at 0x000001F8C66FA1D0>, <weakref at 0x000001F8C6994778; to 'A' at 0x000001F8C66FA1D0>] 数量: 2
# c指向的对象: <__main__.A object at 0x000001F8C66FA1D0>
# 对象A被删除!
# c指向的对象: None

参考:https://www.jianshu.com/p/b94b054b8a5d

弱引用字典
import weakref

class A: pass
a = A()
b = A()
w = weakref.WeakValueDictionary()
# w = {}
# 将w改成字典,则会发现a没有被回收
w["a"] = a
w["b"] = b
print(list(w.keys()))
del a
print(list(w.keys()))

# ['a', 'b']
# ['b']

可以看到将a删除以后,弱引用字典里的a也被删除,从而起到一个类似缓冲的作用

参考:https://blog.csdn.net/MZP_man/article/details/99236003

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容

  • 在中国,一个地方从荒凉走向繁华要多久? 2013年5月。在公司附近找房子,中介推荐去一处新开发的楼盘。那一片政府准...
    满九阅读 121评论 0 0
  • 此文主要以证书生成配置为主,实现简单推送,部分截图与内容来自于互联网,若对大家有所帮助,还请给个赞O(∩_∩)O~...
    damonzero1991阅读 406评论 0 2
  • 百岭自回合,天开宝树林。 古幢灵影曳,风竹涧泉吟。 白石参龙象,青山习道心。 网罗空绻恋,吾意在高深。
    江南莫之阅读 807评论 0 12
  • ionic.css文件中替换以下css
    一只飞阅读 549评论 0 0