如果寫過C和C++的小夥伴肯定都知道,程序中的內存管理是非常關鍵的,一不小心可能就會產生內存洩漏。但是我們在寫Python的時候好像從來沒有關心過內存的處理,為什麼可以這麼爽?在你爽的背後,實際上是Python在默默的幫你管理著,具體怎麼實現的,聽我慢慢道來。
一、引用計數:
在Python中,使用了引用計數這一技術實現內存管理。一個對象被創建完成後就有一個變量指向他,那麼就說明他的引用計數為1,以後如果有其他變量指向他,引用計數也會相應增加,如果將一個變量不再執行這個對象,那麼這個對象的引用計數減1。如果一個對象沒有任何變量指向他,也即引用計數為0,那麼這個對象會被Python回收。示例代碼如下:
<code>class Person(object): def __init__(self,name): self.name = name def __del__(self): print('%s執行了del函數'%self.name) while True: p1 = Person('p1') p2 = Person('p2') del p1 del p2 a = input('test:')/<code>
可以看到,兩個對象的del函數都得到了執行。
二、循環引用:
引用計數這一技術雖然可以在一定程度上解決內存管理的問題。但是還是有不能解決的問題,即循環引用。比如現在有兩個對象分別為a和b,a指向了b,b又指向了a,那麼他們兩的引用計數永遠都不會為0。也即永遠得不到回收。看以下示例:
<code>class Person(object): def __init__(self,name): self.name = name def __del__(self): print('%s執行了del函數'%self.name) while True: p1 = Person('p1') p2 = Person('p2') # 循環引用後,永遠得不到釋放 p1.next = p2 p2.prev = p1 del p1 del p2 a = input('test:')/<code>
可以看到,del函數是不會運行的,是因為循環引用導致兩個對象得不到釋放。
三、標記清除和分代回收:
在Python程序中,每次你新創建了一個對象,那麼就會將這個對象掛到一個叫做零代鏈表中(當然這個鏈表是Python內部的,Python開發者是沒法訪問到的)。比如現在你在程序中創建四個Person對象,分別叫做p1、p2、p3以及p4,然後p1與p2之間互相引用,並且讓p3和p4的引用計數為2,示例代碼如下:
<code>import sys class Person(object): def __init__(self,name): self.name = name self.next = None self.prev = None p1 = Person('p1') p2 = Person('p2') p3 = Person('p3') p4 = Person('p4') p1.next = p2 p2.prev = p1 temp1 = p3 temp2 = p4 print(sys.getrefcount(p1)) print(sys.getrefcount(p2)) print(sys.getrefcount(p3)) print(sys.getrefcount(p4))/<code>
以上代碼實際上就會將p1和p2以及p3和p4掛在一個叫做零代鏈表中,示例圖如下:
我們可以看到,這時候p1引用了p2,而p2又引用了p1,因此這兩個對象產生了循環引用。在後期即使我刪除了del p1以及del p2,那麼這兩個對象也會得不到釋放。
因此這時候Python就啟用了一個新的垃圾回收的機制。如果創建的對象總和減去被釋放的對象,達到一定的值(某個閾值),那麼Python就會遍歷這個零代鏈表,找到那些有相互引用的對象,將這些對象的引用計數減1,如果引用計數值為0了,那麼就說明這個對象是可以被釋放的,比如以上p1和p2,這時候就會釋放p1和p2。接下來再將沒有被釋放的對象,挪動到一個新的鏈表中,這個鏈表叫做一代鏈表。
在零代鏈表清理的次數達到某個閾值後,Python會去遍歷一代鏈表,將那些沒有得到釋放的對象移動到二代鏈表。同樣的原理,如果一代鏈表清理的次數達到某個閾值後,Python會去遍歷二代鏈表,把垃圾對象進行回收。
四、弱代假說:
來看看代垃圾回收算法的核心行為:垃圾回收器會更頻繁的處理新對象。一個新的對象即是你的程序剛剛創建的,而一個來的對象則是經過了幾個時間週期之後仍然存在的對象。Python會在當一個對象從零代移動到一代,或是從一代移動到二代的過程中提升(promote)這個對象。
為什麼要這麼做?這種算法的根源來自於弱代假說(weak generational hypothesis)。這個假說由兩個觀點構成:首先是年輕的對象通常死得也快,而老對象則很有可能存活更長的時間。
假定現在我用Python創建一個新對象:
根據假說,我的代碼很可能僅僅會使用ABC很短的時間。這個對象也許僅僅只是一個方法中的中間結果,並且隨著方法的返回這個對象就將變成垃圾了。大部分的新對象都是如此般地很快變成垃圾。然而,偶爾程序會創建一些很重要的,存活時間比較長的對象-例如web應用中的session變量或是配置項。
通過頻繁的處理零代鏈表中的新對象,Python的垃圾收集器將把時間花在更有意義的地方:它處理那些很快就可能變成垃圾的新對象。同時只在很少的時候,當滿足閾值的條件,收集器才回去處理那些老變量。