內(nèi)存管理機制
可變對象/不可變對象
- 可變對象:如列表、字典等全陨,本質(zhì)是不論怎么改變值爆班,他的地址都不會發(fā)生改變。
- 不可變對象:如int辱姨、float柿菩、bool、str雨涛、元組等枢舶,當(dāng)你重新賦值時,在底層他將會修改指向的數(shù)據(jù)镜悉,而不會對原來指向的值進行修改祟辟,對原來的值修改的僅時引用次數(shù)減一
內(nèi)存分配原則
python中一切皆對象,在底層中每個對象都是基于結(jié)構(gòu)體實現(xiàn)的侣肄,而這些結(jié)構(gòu)體里都有著上下指向旧困、引用計數(shù)和數(shù)據(jù)類型的屬性,在初始化時都會給數(shù)據(jù)的引用計數(shù)設(shè)置為1稼锅,每當(dāng)多一個指向時吼具,其引用計數(shù)就加一,每刪除一個他的指向矩距,就引用計數(shù)減一拗盒,當(dāng)為0時就會對該數(shù)據(jù)進行垃圾回收
緩存機制
在python的內(nèi)存管理當(dāng)中可能存在一些緩存機制(如:int、float锥债、str陡蝇、list等都有),即:將某個數(shù)據(jù)刪除時哮肚,其可能不會將這個對象完全銷毀登夫,而是將對象存放到一個鏈表中,當(dāng)又創(chuàng)建同類型的對象時允趟,將會直接賦值給緩存的同類型對象恼策,再通過變量引用指向他,舉例:
>>> a = "xxx"
>>> id(a)
2436976108520
>>> del a
# 刪除了字符串a(chǎn)
>>> b = "yyy"
# 創(chuàng)建字符串b
>>> id(b)
# 可以發(fā)現(xiàn)內(nèi)存地址時一樣的
2436976108520
垃圾回收機制
對象引用次數(shù)
一般情況下垃圾回收基于對象引用次數(shù)潮剪,當(dāng)初始化時次數(shù)為1涣楷,被其他對象引用時加1,使用del
本質(zhì)則是將引用次數(shù)減一抗碰,而當(dāng)引用次數(shù)變成0以后則會自動觸發(fā)垃圾回收機制將其回收狮斗,代碼層面上則是會調(diào)用該對象的__del__
方法,舉例:
class D(dict):
def __init__(self, name):
self.name = name
print("對象:{}被創(chuàng)建弧蝇!".format(self.name))
def __del__(self):
print("對象:{}被銷毀碳褒!".format(self.name))
a = D('a')
a = D('b')
print("程序結(jié)束迄汛!")
結(jié)果:
對象:a被創(chuàng)建!
對象:b被創(chuàng)建骤视!
對象:a被銷毀!
程序結(jié)束鹃觉!
對象:b被銷毀专酗!
可以看出第一句實例化A時對象被創(chuàng)建,對象的引用計數(shù)初始化為1盗扇,當(dāng)?shù)诙鋱?zhí)行時祷肯,新的A對象被創(chuàng)建,新的對象引用計數(shù)為1疗隶,而舊的A對象因為a指向了其他數(shù)據(jù)佑笋,所以引用次數(shù)減一,此時舊的A對象引用次數(shù)變成0斑鼻,觸發(fā)銷毀機制蒋纬,從而自動調(diào)用了__del__
方法。當(dāng)程序結(jié)束時坚弱,因為要回收內(nèi)存蜀备,因此新的對象A也自動調(diào)用__del__
方法。
查看引用次數(shù)
可以用sys.getrefcount()
方法來查看引用次數(shù)荒叶,要注意因為將內(nèi)容傳入該方法時引用也會加1碾阁,所以我們實際想知道的引用次數(shù)應(yīng)該是輸出的結(jié)果減一,舉例:
>>> a = 1000
>>> sys.getrefcount(a)
# 加上傳入方法的a些楣,引用次數(shù)為2
2
>>> b = a
>>> sys.getrefcount(a)
# 因為b也引用了a脂凶,所以引用次數(shù)加1
3
>>> del b
>>> sys.getrefcount(a)
# 刪除了b以后引用次數(shù)減1
2
標記清除
前面的引用計數(shù)能夠解決一般情況下的內(nèi)存回收問題,但是對于循環(huán)引用的情況愁茁,可能就會無法回收蚕钦,從而造成內(nèi)存泄漏的問題,例如下面代碼:
class D(dict):
def __init__(self, name):
self.name = name
print("對象:{}被創(chuàng)建埋市!".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("程序結(jié)束!")
結(jié)果:
對象:a被創(chuàng)建道宅!
對象:b被創(chuàng)建食听!
程序結(jié)束!
對象:a被銷毀污茵!
對象:b被銷毀樱报!
可以看到上面的兩個字典類因為互相指向了,所以即使銷毀了泞当,引用計數(shù)也永遠大于0迹蛤,此時垃圾回收機制也就不起作用了,所以之后即使a和b都指向了其他值,但因為他們原先指向的字典類互相有指向盗飒,引用計數(shù)不為0嚷量,導(dǎo)致他們直到程序結(jié)束內(nèi)存才被回收。此時如果想要回收逆趣,那么就需要先收集垃圾蝶溶,然后再進行回收,Python提供了gc.collect()
方法用于手動回收數(shù)據(jù)宣渗,舉例:
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
可以看到兩個互相引用的對象被回收了,而這種手動回收的方式就基于了標記清除來實現(xiàn):
- 首先GC會對所有活動的對象打上標記痕囱,即一個個點田轧,然后他們之間的引用通過指向來表明,此時就構(gòu)成了一個有向圖
- 然后GC會從根對象出發(fā)鞍恢,沿著有向邊遍歷整個圖傻粘,而對于不可達的對象,那么就被視為需要清理的垃圾對象有序。
分代回收
建立在垃圾清除的基礎(chǔ)上抹腿,其將對象的活動時間分為3代,新生的對象在0代旭寿,如果他們在第0代中能夠存活下來警绩,就會被放入1代里,當(dāng)在1代中也存活了下來盅称,再被放到2代肩祥,默認當(dāng)對象數(shù)量減去釋放的對象數(shù)量(即當(dāng)前可達的對象數(shù)量)超過700時將會對0代對象進行回收處理,當(dāng)進行了10次0代回收則會觸發(fā)1代回收缩膝,當(dāng)進行了10次1代回收則會觸發(fā)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)
這里我們再來對前面循環(huán)引用的情況通過分代回收來查看效果将饺,首先由于默認的設(shè)置里是需要對象數(shù)量減去釋放數(shù)量超過700時才會觸發(fā),而這里我們使用的對象示例較少痛黎,所以需要我們調(diào)整這個觸發(fā)的閾值予弧,然后為了更加明顯地看出回收的步驟,這里也重寫了__new__
方法湖饱,代碼如下:
import gc
gc.set_threshold(2, 10, 10)
# 第一個參數(shù)代表掖蛤,如果設(shè)置為0代表禁用,這里設(shè)置2井厌,代表第0代超過2個對象時觸發(fā)垃圾回收
# 后面兩個是對第一代和第二代的進行回收蚓庭,這里只要大于1就行了
# 等于1的話那么會不停觸發(fā)對1/2代的回收致讥,從而導(dǎo)致對第0代的回收失敗
class D(dict):
def __new__(self, name):
print("對象:{}被分配!".format(name))
return dict.__new__(self)
def __init__(self, name):
self.name = name
print("對象:{}被創(chuàng)建器赞!".format(self.name))
def __del__(self):
print("對象:{}被銷毀垢袱!".format(self.name))
print("初始時的垃圾回收計數(shù)器:", gc.get_count())
a = D('a')
b = D('b')
print("創(chuàng)建了兩個對象時的回收計數(shù)器:", gc.get_count())
a['x'] = b
b['x'] = a
a = 1
b = 2
print("修改了兩個對象時的垃圾回收計數(shù)器:", gc.get_count())
c = D('c')
# 分配空間給C時,可以看到觸發(fā)了第0代的回收
print("新分配空間給對象C時的垃圾回收計數(shù)器:", gc.get_count())
print("程序結(jié)束港柜!")
結(jié)果:
初始時的垃圾回收計數(shù)器: (0, 8, 1)
對象:a被分配惶桐!
對象:a被創(chuàng)建!
對象:b被分配潘懊!
對象:b被創(chuàng)建!
創(chuàng)建了兩個對象時的回收計數(shù)器: (2, 8, 1)
修改了兩個對象時的垃圾回收計數(shù)器: (2, 8, 1)
對象:c被分配贿衍!
對象:a被銷毀授舟!
對象:b被銷毀!
對象:c被創(chuàng)建贸辈!
新分配空間給對象C時的垃圾回收計數(shù)器: (0, 9, 1)
程序結(jié)束释树!
對象:c被銷毀!
可以看出在我們的主要代碼跑起前已經(jīng)進行過8次1代和1次2代的垃圾回收了擎淤,當(dāng)創(chuàng)建了兩個對象以后奢啥,0代增加了2個,修改了這兩個對象的指向后嘴拢,計數(shù)器看起來還是2個a和b桩盲,但是實際上因為原來的兩個字典循環(huán)引用導(dǎo)致未被釋放,所以實際有4個席吴,只是有2個是不可達的赌结,因此在給對象c分配空間時計數(shù)器增加1變成3,因為超過了2孝冒,需要進行一次對0代的垃圾回收柬姚,因此a和b這兩個不可達的就被銷毀,然后再創(chuàng)建對象c庄涡,最終程序結(jié)束量承,將未被釋放的對象a、b和c都銷毀
更多參考:
https://blog.csdn.net/it_yuan/article/details/52850270
https://www.jb51.net/article/79306.htm
http://www.reibang.com/p/0c37059ce224
https://testerhome.com/topics/16556
弱引用
當(dāng)引用某個數(shù)據(jù)時穴店,引用計數(shù)不會加一撕捍,假如有些數(shù)據(jù)被刪除后,希望直接被垃圾回收迹鹅,就可以利用弱引用來實現(xiàn)卦洽,舉例:
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)
# 創(chuàng)建一個弱引用集合
s = weakref.WeakSet()
# 往集合當(dāng)中添加一個對a的弱引用
s.add(a)
print("a的弱引用:", weakref.getweakrefs(a), "數(shù)量:", 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>] 數(shù)量: 2
# c指向的對象: <__main__.A object at 0x000001F8C66FA1D0>
# 對象A被刪除斜棚!
# c指向的對象: None
弱引用字典
import weakref
class A: pass
a = A()
b = A()
w = weakref.WeakValueDictionary()
# w = {}
# 將w改成字典阀蒂,則會發(fā)現(xiàn)a沒有被回收
w["a"] = a
w["b"] = b
print(list(w.keys()))
del a
print(list(w.keys()))
# ['a', 'b']
# ['b']
可以看到將a
刪除以后该窗,弱引用字典里的a
也被刪除,從而起到一個類似緩沖的作用