Python中的“垃圾”是怎么回收的种樱?

前言

對于python來說,一切皆為對象俊卤,所有的變量賦值都遵循著對象引用機制嫩挤。程序在運行的時候,需要在內存中開辟出一塊空間瘾蛋,用于存放運行時產生的臨時變量俐镐;計算完成后,再將結果輸出到永久性存儲器中哺哼。如果數據量過大佩抹,內存空間管理不善就很容易出現 OOM(out of memory),俗稱爆內存取董,程序可能被操作系統(tǒng)中止棍苹。

而對于服務器,內存管理則顯得更為重要茵汰,不然很容易引發(fā)內存泄漏- 這里的泄漏枢里,并不是說你的內存出現了信息安全問題,被惡意程序利用了,而是指程序本身沒有設計好栏豺,導致程序未能釋放已不再使用的內存彬碱。- 內存泄漏也不是指你的內存在物理上消失了,而是意味著代碼在分配了某段內存后奥洼,因為設計錯誤巷疼,失去了對這段內存的控制,從而造成了內存的浪費灵奖。也就是這塊內存脫離了gc的控制

計數引用

因為python中一切皆為對象嚼沿,你所看到的一切變量,本質上都是對象的一個指針瓷患。

當一個對象不再調用的時候骡尽,也就是當這個對象的引用計數(指針數)為 0 的時候,說明這個對象永不可達擅编,自然它也就成為了垃圾攀细,需要被回收“可以簡單的理解為沒有任何變量再指向它辨图。

import os

import psutil

# 顯示當前 python 程序占用的內存大小

def show_memory_info(hint):

? ? pid = os.getpid()

? ? p = psutil.Process(pid)

? ? info = p.memory_full_info()

? ? memory = info.uss / 1024. / 1024

? ? print('{} memory used: {} MB'.format(hint, memory))

def func():

? ? show_memory_info('initial')

? ? a = [i for i in range(10000000)]

? ? show_memory_info('after a created')

func()

show_memory_info('finished')

########## 輸出 ##########

initial memory used: 47.19140625 MB

after a created memory used: 433.91015625 MB

finished memory used: 48.109375 MB

可以看到調用函數 func(),在列表 a 被創(chuàng)建之后肢藐,內存占用迅速增加到了 433 MB:而在函數調用結束后故河,內存則返回正常。這是因為吆豹,函數內部聲明的列表 a 是局部變量鱼的,在函數返回后,局部變量的引用會注銷掉痘煤;此時凑阶,列表 a 所指代對象的引用數為 0,Python 便會執(zhí)行垃圾回收衷快,因此之前占用的大量內存就又回來了宙橱。

def func():

? ? show_memory_info('initial')

? ? global a

? ? a = [i for i in range(10000000)]

? ? show_memory_info('after a created')

func()

show_memory_info('finished')

########## 輸出 ##########

initial memory used: 48.88671875 MB

after a created memory used: 433.94921875 MB

finished memory used: 433.94921875 MB

新的這段代碼中,global a 表示將 a 聲明為全局變量蘸拔。那么师郑,即使函數返回后,列表的引用依然存在调窍,于是對象就不會被垃圾回收掉宝冕,依然占用大量內存。同樣邓萨,如果我們把生成的列表返回地梨,然后在主程序中接收菊卷,那么引用依然存在,垃圾回收就不會被觸發(fā)宝剖,大量內存仍然被占用著:

def func():

? ? show_memory_info('initial')

? ? a = [i for i in derange(10000000)]

? ? show_memory_info('after a created')

? ? return a

a = func()

show_memory_info('finished')

########## 輸出 ##########

initial memory used: 47.96484375 MB

after a created memory used: 434.515625 MB

finished memory used: 434.515625 MB

那怎么可以看到變量被引用了多少次呢洁闰?通過sys.getrefcount

import sys

a = []

# 兩次引用,一次來自 a万细,一次來自 getrefcount

print(sys.getrefcount(a))

def func(a):

? ? # 四次引用渴庆,a,python 的函數調用棧雅镊,函數參數,和 getrefcount

? ? print(sys.getrefcount(a))

func(a)

# 兩次引用刃滓,一次來自 a仁烹,一次來自 getrefcount,函數 func 調用已經不存在

print(sys.getrefcount(a))

########## 輸出 ##########

2

4

2

如果其中涉及函數調用咧虎,會額外增加兩次1. 函數棧2. 函數調用

從這里就可以看到python不再需要像C那種的認為的釋放內存卓缰,但是python同樣給我們提供了手動釋放內存的方法gc.collect()

import gc

show_memory_info('initial')

a = [i for i in range(10000000)]

show_memory_info('after a created')

del a

gc.collect()

show_memory_info('finish')

print(a)

########## 輸出 ##########

initial memory used: 48.1015625 MB

after a created memory used: 434.3828125 MB

finish memory used: 48.33203125 MB

---------------------------------------------------------------------------

NameError? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? Traceback (most recent call last)

<ipython-input-12-153e15063d8a> in <module>

? ? 11

? ? 12 show_memory_info('finish')

---> 13 print(a)

NameError: name 'a' is not defined

截止目前,貌似python的垃圾回收機制非常的簡單砰诵,只要對象引用次數為0征唬,必定為觸發(fā)gc,那么引用次數為0是否是觸發(fā)gc的充要條件呢茁彭?

循環(huán)回收

如果有兩個對象总寒,它們互相引用,并且不再被別的對象所引用理肺,那么它們應該被垃圾回收嗎摄闸?

def func():

? ? show_memory_info('initial')

? ? a = [i for i in range(10000000)]

? ? b = [i for i in range(10000000)]

? ? show_memory_info('after a, b created')

? ? a.append(b)

? ? b.append(a)

func()

show_memory_info('finished')

########## 輸出 ##########

initial memory used: 47.984375 MB

after a, b created memory used: 822.73828125 MB

finished memory used: 821.73046875 MB

從結果顯而易見,它們并沒有被回收妹萨,但是從程序上來看年枕,當這個函數結束的時候,作為局部變量的a乎完,b就已經從程序意義上不存在了熏兄。但是因為它們的互相引用,導致了它們的引用數都不為0树姨。

這時要如何規(guī)避呢1. 從代碼邏輯上進行整改摩桶,避免這種循環(huán)引用2. 通過人工回收

import gc

def func():

? ? show_memory_info('initial')

? ? a = [i for i in range(10000000)]

? ? b = [i for i in range(10000000)]

? ? show_memory_info('after a, b created')

? ? a.append(b)

? ? b.append(a)

func()

gc.collect()

show_memory_info('finished')

########## 輸出 ##########

initial memory used: 49.51171875 MB

after a, b created memory used: 824.1328125 MB

finished memory used: 49.98046875 MB

python針對循環(huán)引用,有它的自動垃圾回收算法1. 標記清除(mark-sweep)算法2. 分代收集(generational)

標記清除

標記清除的步驟總結為如下步驟1. GC會把所有的『活動對象』打上標記2. 把那些沒有標記的對象『非活動對象』進行回收

那么python如何判斷何為非活動對象帽揪?

通過用圖論來理解不可達的概念典格。對于一個有向圖,如果從一個節(jié)點出發(fā)進行遍歷台丛,并標記其經過的所有節(jié)點耍缴;那么砾肺,在遍歷結束后,所有沒有被標記的節(jié)點防嗡,我們就稱之為不可達節(jié)點变汪。顯而易見,這些節(jié)點的存在是沒有任何意義的蚁趁,自然的裙盾,我們就需要對它們進行垃圾回收。

但是每次都遍歷全圖他嫡,對于 Python 而言是一種巨大的性能浪費番官。所以,在 Python 的垃圾回收實現中钢属,mark-sweep 使用雙向鏈表維護了一個數據結構徘熔,并且只考慮容器類的對象(只有容器類對象,list淆党、dict酷师、tuple,instance染乌,才有可能產生循環(huán)引用)山孔。

圖中把小黑圈視為全局變量,也就是把它作為root object荷憋,從小黑圈出發(fā)台颠,對象1可直達,那么它將被標記勒庄,對象2蓉媳、3可間接到達也會被標記,而4和5不可達锅铅,那么1酪呻、2、3就是活動對象盐须,4和5是非活動對象會被GC回收玩荠。

分代回收

分代回收是一種以空間換時間的操作方式,Python將內存根據對象的存活時間劃分為不同的集合贼邓,每個集合稱為一個代阶冈,Python將內存分為了3“代”,分別為年輕代(第0代)塑径、中年代(第1代)女坑、老年代(第2代),他們對應的是3個鏈表统舀,它們的垃圾收集頻率與對象的存活時間的增大而減小匆骗。新創(chuàng)建的對象都會分配在年輕代劳景,年輕代鏈表的總數達到上限時(當垃圾回收器中新增對象減去刪除對象達到相應的閾值時),Python垃圾收集機制就會被觸發(fā)碉就,把那些可以被回收的對象回收掉盟广,而那些不會回收的對象就會被移到中年代去,依此類推瓮钥,老年代中的對象是存活時間最久的對象筋量,甚至是存活于整個系統(tǒng)的生命周期內。同時碉熄,分代回收是建立在標記清除技術基礎之上桨武。

事實上,分代回收基于的思想是锈津,新生的對象更有可能被垃圾回收呀酸,而存活更久的對象也有更高的概率繼續(xù)存活。因此一姿,通過這種做法,可以節(jié)約不少計算量跃惫,從而提高 Python 的性能叮叹。

所以對于剛剛的問題,引用計數只是觸發(fā)gc的一個充分非必要條件爆存,循環(huán)引用同樣也會觸發(fā)蛉顽。

調試

可以使用 objgraph來調試程序,因為目前它的官方文檔先较,還沒有細讀携冤,只能把文檔放在這供大家參閱啦~其中兩個函數非常有用1. show_refs()2. show_backrefs()

總結

1.垃圾回收是 Python 自帶的機制,用于自動釋放不會再用到的內存空間闲勺;

2.引用計數是其中最簡單的實現曾棕,不過切記,這只是充分非必要條件菜循,因為循環(huán)引用需要通過不可達判定翘地,來確定是否可以回收;

3.Python 的自動回收算法包括標記清除和分代回收癌幕,主要針對的是循環(huán)引用的垃圾收集衙耕;

4.調試內存泄漏方面, objgraph 是很好的可視化分析工具勺远。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末橙喘,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子胶逢,更是在濱河造成了極大的恐慌厅瞎,老刑警劉巖饰潜,帶你破解...
    沈念sama閱讀 223,207評論 6 521
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異磁奖,居然都是意外死亡囊拜,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 95,455評論 3 400
  • 文/潘曉璐 我一進店門比搭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來冠跷,“玉大人,你說我怎么就攤上這事身诺∶弁校” “怎么了?”我有些...
    開封第一講書人閱讀 170,031評論 0 366
  • 文/不壞的土叔 我叫張陵霉赡,是天一觀的道長橄务。 經常有香客問我,道長穴亏,這世上最難降的妖魔是什么蜂挪? 我笑而不...
    開封第一講書人閱讀 60,334評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮嗓化,結果婚禮上棠涮,老公的妹妹穿的比我還像新娘。我一直安慰自己刺覆,他們只是感情好严肪,可當我...
    茶點故事閱讀 69,322評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著谦屑,像睡著了一般驳糯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上氢橙,一...
    開封第一講書人閱讀 52,895評論 1 314
  • 那天酝枢,我揣著相機與錄音,去河邊找鬼悍手。 笑死隧枫,一個胖子當著我的面吹牛,可吹牛的內容都是我干的谓苟。 我是一名探鬼主播官脓,決...
    沈念sama閱讀 41,300評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼涝焙!你這毒婦竟也來了卑笨?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 40,264評論 0 277
  • 序言:老撾萬榮一對情侶失蹤仑撞,失蹤者是張志新(化名)和其女友劉穎赤兴,沒想到半個月后妖滔,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 46,784評論 1 321
  • 正文 獨居荒郊野嶺守林人離奇死亡桶良,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,870評論 3 343
  • 正文 我和宋清朗相戀三年座舍,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片陨帆。...
    茶點故事閱讀 40,989評論 1 354
  • 序言:一個原本活蹦亂跳的男人離奇死亡曲秉,死狀恐怖,靈堂內的尸體忽然破棺而出疲牵,到底是詐尸還是另有隱情承二,我是刑警寧澤,帶...
    沈念sama閱讀 36,649評論 5 351
  • 正文 年R本政府宣布纲爸,位于F島的核電站亥鸠,受9級特大地震影響,放射性物質發(fā)生泄漏识啦。R本人自食惡果不足惜负蚊,卻給世界環(huán)境...
    茶點故事閱讀 42,331評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望颓哮。 院中可真熱鬧家妆,春花似錦、人聲如沸题翻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,814評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嵌赠。三九已至,卻和暖如春熄赡,著一層夾襖步出監(jiān)牢的瞬間姜挺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,940評論 1 275
  • 我被黑心中介騙來泰國打工彼硫, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留炊豪,地道東北人。 一個月前我還...
    沈念sama閱讀 49,452評論 3 379
  • 正文 我出身青樓拧篮,卻偏偏與公主長得像词渤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子串绩,可洞房花燭夜當晚...
    茶點故事閱讀 45,995評論 2 361

推薦閱讀更多精彩內容