Python 內(nèi)存管理和垃圾回收機制

內(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):

  1. 首先GC會對所有活動的對象打上標記痕囱,即一個個點田轧,然后他們之間的引用通過指向來表明,此時就構(gòu)成了一個有向圖
  2. 然后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

參考:http://www.reibang.com/p/b94b054b8a5d

弱引用字典
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也被刪除,從而起到一個類似緩沖的作用

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蚤霞,一起剝皮案震驚了整個濱河市酗失,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌昧绣,老刑警劉巖规肴,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異夜畴,居然都是意外死亡拖刃,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門贪绘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來兑牡,“玉大人,你說我怎么就攤上這事税灌【” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵菱涤,是天一觀的道長苞也。 經(jīng)常有香客問我,道長粘秆,這世上最難降的妖魔是什么如迟? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮攻走,結(jié)果婚禮上氓涣,老公的妹妹穿的比我還像新娘。我一直安慰自己陋气,他們只是感情好劳吠,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著巩趁,像睡著了一般痒玩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上议慰,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天蠢古,我揣著相機與錄音,去河邊找鬼别凹。 笑死草讶,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的炉菲。 我是一名探鬼主播堕战,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼坤溃,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了嘱丢?” 一聲冷哼從身側(cè)響起薪介,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎越驻,沒想到半個月后汁政,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡缀旁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年记劈,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片并巍。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡抠蚣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出履澳,到底是詐尸還是另有隱情,我是刑警寧澤怀跛,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布距贷,位于F島的核電站,受9級特大地震影響吻谋,放射性物質(zhì)發(fā)生泄漏忠蝗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一漓拾、第九天 我趴在偏房一處隱蔽的房頂上張望阁最。 院中可真熱鬧,春花似錦骇两、人聲如沸速种。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽配阵。三九已至,卻和暖如春示血,著一層夾襖步出監(jiān)牢的瞬間棋傍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工难审, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留瘫拣,地道東北人。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓告喊,卻偏偏與公主長得像麸拄,于是被迫代替她去往敵國和親派昧。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 在中國感帅,一個地方從荒涼走向繁華要多久斗锭? 2013年5月。在公司附近找房子失球,中介推薦去一處新開發(fā)的樓盤岖是。那一片政府準...
    滿九閱讀 123評論 0 0
  • 此文主要以證書生成配置為主,實現(xiàn)簡單推送实苞,部分截圖與內(nèi)容來自于互聯(lián)網(wǎng),若對大家有所幫助,還請給個贊O(∩_∩)O~...
    damonzero1991閱讀 412評論 0 2
  • 百嶺自回合豺撑,天開寶樹林。 古幢靈影曳黔牵,風(fēng)竹澗泉吟聪轿。 白石參龍象,青山習(xí)道心猾浦。 網(wǎng)羅空綣戀陆错,吾意在高深。
    江南莫之閱讀 813評論 0 12
  • ionic.css文件中替換以下css
    一只飛閱讀 554評論 0 0