python內(nèi)存管理
1. 引用和對(duì)象
??我們先看這樣一個(gè)賦值語(yǔ)句 a=1
??在 python 中把介,整數(shù) 1 為一個(gè)對(duì)象定躏,而 a 是一個(gè)引用 a→1
??Python是動(dòng)態(tài)類型的語(yǔ)言(動(dòng)態(tài)類型)万矾,對(duì)象與引用分離。Python通過(guò)引用來(lái)操作對(duì)象痛悯。
??在Python中嵌巷,整數(shù)和短小的字符,Python都會(huì)緩存這些對(duì)象丹皱,以便重復(fù)使用妒穴。
# id()是Python內(nèi)置的函數(shù),它用于返回對(duì)象的身份(identity)摊崭,也就是內(nèi)存地址讼油。
# hex 返回一個(gè)數(shù)的十六進(jìn)制表示
a = 1
b = 1
print(id(1))
print(id(a))
print(id(b))
print(hex(id(a)))
1644917568
1644917568
1644917568
0x620b7340
??從上邊代碼的輸出可以看出,a 和 b 其實(shí)是指向同一個(gè)對(duì)象的兩個(gè)引用呢簸。
??我們現(xiàn)在使用 is 來(lái)檢測(cè)一下矮台,整數(shù)和短小的字符指的是什么。
a = 1
b = 1
print(a is b)
True
a = 'good'
b = 'good'
print(a is b)
True
a = 'good night'
b = 'good night'
print(a is b)
print(a[:4] is 'good')
print(a[:4] is b[:4])
False
False
False
a = [1]
b = [1]
print(a is b)
False
??可以看到根时,python緩存了整數(shù)和短小的字符瘦赫,所以指向它們的引用都是指向的緩存好的對(duì)象。
??對(duì)于較長(zhǎng)的字符蛤迎,則是另外分配的內(nèi)存耸彪,即使是相同的字符串的引用,由于它們指向的對(duì)象是另外分配的內(nèi)存忘苛,它們的內(nèi)存地址也是不同的。
a = 'good night'
b = 'good night'
print(hex(id(a)))
print(hex(id(b)))
0x19b922e1070
0x19b922e17b0
??在 Python 中唱较,每個(gè)對(duì)象都有指向該對(duì)象的引用計(jì)數(shù)扎唾。我們可以使用 sys.getrefcount() 來(lái)查看某個(gè)對(duì)象的引用計(jì)數(shù),參數(shù)傳遞給 getrefcount()時(shí)南缓,會(huì)創(chuàng)建一個(gè)臨時(shí)的引用胸遇,所以 getrefcount() 會(huì)比期望結(jié)果多 1。
import sys
a = [1, 2]
sys.getrefcount(a)
2
??Python 中的容器對(duì)象汉形,如 list纸镊、tuple、dict 等概疆,可以包含多個(gè)對(duì)象逗威。實(shí)際上,它們包含的只是對(duì)象的引用而已岔冀。
a = [1, 2]
b = [a]
print(b)
[[1, 2]]
a[0] = -1
print(b)
[[-1, 2]]
??使用 del 可以刪除一個(gè)引用凯旭,同時(shí)也會(huì)減少相應(yīng)對(duì)象的引用計(jì)數(shù)。此外,將引用指向別的對(duì)象也會(huì)使引用計(jì)數(shù)減少罐呼。
a = [1]
b = a
c = b
print( sys.getrefcount(c) )
del a # del 刪除變量鞠柄,減少引用計(jì)數(shù)
print( sys.getrefcount(c) )
b = None # 指向別的對(duì)象,減少引用計(jì)數(shù)
print( sys.getrefcount(c) )
4
3
2
??兩個(gè)對(duì)象還可以相互引用嫉柴,這樣會(huì)形成一個(gè)引用環(huán)厌杜。
a = [1]
b = [a]
a.append(b)
print(a)
[1, [[...]]]
??一個(gè)對(duì)象也有可能會(huì)形成引用環(huán)。
a = [1]
a.append(a) # 如果是 a = [a]计螺,則不會(huì)形成引用環(huán)夯尽,此時(shí) a 為 [[1]]
print(a)
[1, [...]]
2.垃圾回收
??當(dāng)Python中的對(duì)象越來(lái)越多,它們將占據(jù)越來(lái)越大的內(nèi)存危尿,這個(gè)時(shí)候就需要垃圾回收(Garbage Collection)了呐萌。當(dāng)Python的某個(gè)對(duì)象的引用計(jì)數(shù)降為0時(shí),說(shuō)明沒有任何引用指向該對(duì)象谊娇,該對(duì)象就成為要被回收的垃圾了肺孤。
??不過(guò),垃圾回收時(shí)济欢,Python不能進(jìn)行其它的任務(wù)赠堵。頻繁的垃圾回收將大大降低Python的工作效率。如果內(nèi)存中的對(duì)象不多法褥,就沒有必要總啟動(dòng)垃圾回收茫叭。所以,Python只會(huì)在特定條件下半等,自動(dòng)啟動(dòng)垃圾回收揍愁。當(dāng)Python運(yùn)行時(shí),會(huì)記錄其中分配對(duì)象(object allocation)和取消分配對(duì)象(object deallocation)的次數(shù)杀饵。當(dāng)兩者的差值高于某個(gè)閾值時(shí)莽囤,垃圾回收才會(huì)啟動(dòng)。我們可以通過(guò)gc模塊的get_threshold()方法切距,查看該閾值:
import gc
print(gc.get_threshold())
(700, 10, 10)
??后面的兩個(gè)10是與分代回收相關(guān)的閾值朽缎。700即是垃圾回收啟動(dòng)的閾值,可以通過(guò) gc.set_threshold()
方法重新設(shè)置谜悟。另外话肖,還可以手動(dòng)回收gc.collect()
??另外,引用環(huán)會(huì)給 GC 帶來(lái)很大的麻煩葡幸。對(duì)于上面提到的一個(gè)對(duì)象引用環(huán)的情況最筒,即使刪除了引用 a ,list 對(duì)象的引用計(jì)數(shù)也不會(huì)為0蔚叨。
??Python 采用了分代回收的策略是钥。這一策略的基本假設(shè)是掠归,存活時(shí)間越久的對(duì)象,越不可能在后面的程序中變成垃圾悄泥。我們的程序往往會(huì)產(chǎn)生大量的對(duì)象虏冻,許多對(duì)象很快產(chǎn)生和消失,但也有一些對(duì)象長(zhǎng)期被使用弹囚。出于信任和效率厨相,對(duì)于這樣一些“長(zhǎng)壽”對(duì)象,我們相信它們的用處鸥鹉,所以減少在垃圾回收中掃描它們的頻率蛮穿。
??Python將所有的對(duì)象分為0,1毁渗,2三代践磅。所有的新建對(duì)象都是0代對(duì)象。當(dāng)某一代對(duì)象經(jīng)歷過(guò)垃圾回收灸异,依然存活府适,那么它就被歸入下一代對(duì)象。垃圾回收啟動(dòng)時(shí)肺樟,一定會(huì)掃描所有的0代對(duì)象檐春。如果0代經(jīng)過(guò)一定次數(shù)垃圾回收,那么就啟動(dòng)對(duì)0代和1代的掃描清理么伯。當(dāng)1代也經(jīng)歷了一定次數(shù)的垃圾回收后疟暖,那么會(huì)啟動(dòng)對(duì)0,1田柔,2俐巴,即對(duì)所有對(duì)象進(jìn)行掃描。
??這兩個(gè)次數(shù)即上面get_threshold()返回的(700, 10, 10)返回的兩個(gè)10硬爆。也就是說(shuō)窜骄,每10次0代垃圾回收,會(huì)配合1次1代的垃圾回收摆屯;而每10次1代的垃圾回收,才會(huì)有1次的2代垃圾回收糠亩。
??另外虐骑,引用環(huán)會(huì)給 GC 帶來(lái)很大的麻煩。
a = [1]
a.append(a) # 如果是 a = [a]赎线,則不會(huì)形成引用環(huán)廷没,此時(shí) a 為 [[1]]
print(a)
[1, [...]]
??對(duì)于上面提到的一個(gè)對(duì)象引用環(huán)的情況,即使刪除了引用 a 垂寥,list 對(duì)象的引用計(jì)數(shù)也不會(huì)為0颠黎,不會(huì)被垃圾回收另锋。
??為了回收這樣的引用環(huán),Python復(fù)制每個(gè)對(duì)象的引用計(jì)數(shù)狭归,可以記為 gc_ref夭坪。假設(shè),每個(gè)對(duì)象 i过椎,該計(jì)數(shù)為 gc_ref_i室梅。Python 會(huì)遍歷所有的對(duì)象 i。對(duì)于每個(gè)對(duì)象 i 引用的對(duì)象 j疚宇,將相應(yīng)的 gc_ref_j 減 1亡鼠。在結(jié)束遍歷后,gc_ref 不為0的對(duì)象敷待,和這些對(duì)象引用的對(duì)象间涵,以及繼續(xù)更下游引用的對(duì)象,需要被保留榜揖。而其它的對(duì)象則被垃圾回收勾哩。
??下面例子中 a 指向的對(duì)象的引用計(jì)數(shù)為 2(=3-1),在去除引用 a 后變?yōu)?1根盒,由于對(duì)象引用了本身钳幅,所以它的引用計(jì)數(shù)應(yīng)該減一,即 gc_ref = 0炎滞,這表明改對(duì)象應(yīng)該被垃圾回收敢艰。
a = [1]
a.append(a) # 如果是 a = [a],則不會(huì)形成引用環(huán)册赛,此時(shí) a 為 [[1]]
print(a)
print(sys.getrefcount(a))
[1, [...]]
3