大廠面試必問-Python是如何管理內存的?

如果寫過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掛在一個叫做零代鏈表中,示例圖如下:

大廠面試必問-Python是如何管理內存的?

我們可以看到,這時候p1引用了p2,而p2又引用了p1,因此這兩個對象產生了循環引用。在後期即使我刪除了del p1以及del p2,那麼這兩個對象也會得不到釋放。

大廠面試必問-Python是如何管理內存的?

因此這時候Python就啟用了一個新的垃圾回收的機制。如果創建的對象總和減去被釋放的對象,達到一定的值(某個閾值),那麼Python就會遍歷這個零代鏈表,找到那些有相互引用的對象,將這些對象的引用計數減1,如果引用計數值為0了,那麼就說明這個對象是可以被釋放的,比如以上p1和p2,這時候就會釋放p1和p2。接下來再將沒有被釋放的對象,挪動到一個新的鏈表中,這個鏈表叫做一代鏈表。

大廠面試必問-Python是如何管理內存的?

在零代鏈表清理的次數達到某個閾值後,Python會去遍歷一代鏈表,將那些沒有得到釋放的對象移動到二代鏈表。同樣的原理,如果一代鏈表清理的次數達到某個閾值後,Python會去遍歷二代鏈表,把垃圾對象進行回收。


四、弱代假說:

來看看代垃圾回收算法的核心行為:垃圾回收器會更頻繁的處理新對象。一個新的對象即是你的程序剛剛創建的,而一個來的對象則是經過了幾個時間週期之後仍然存在的對象。Python會在當一個對象從零代移動到一代,或是從一代移動到二代的過程中提升(promote)這個對象。

為什麼要這麼做?這種算法的根源來自於弱代假說(weak generational hypothesis)。這個假說由兩個觀點構成:首先是年輕的對象通常死得也快,而老對象則很有可能存活更長的時間。

假定現在我用Python創建一個新對象:

根據假說,我的代碼很可能僅僅會使用ABC很短的時間。這個對象也許僅僅只是一個方法中的中間結果,並且隨著方法的返回這個對象就將變成垃圾了。大部分的新對象都是如此般地很快變成垃圾。然而,偶爾程序會創建一些很重要的,存活時間比較長的對象-例如web應用中的session變量或是配置項。

通過頻繁的處理零代鏈表中的新對象,Python的垃圾收集器將把時間花在更有意義的地方:它處理那些很快就可能變成垃圾的新對象。同時只在很少的時候,當滿足閾值的條件,收集器才回去處理那些老變量。


分享到:


相關文章: