Python 內存管理機制

本文根據 撩課-Python內存管理機制 整理而成。python

引用計數器機制

當一個對象被引用時,引用計數 +1,當這個對象再也不被引用,或引用它的對象被釋放時,引用計數 -1,當對象的引用計數爲 0 時,釋放該對象。函數

使用 sys.getrefcount(obj) 能夠查看一個對象的當前引用計數。在 Python 中,當對象被傳入到一個函數時,在這個函數的內部有會兩個對象引用着它。可是 sys.getrefcount(obj) 比較特殊,一般只引用一次。性能

class Person:
    pass

def log(obj):
    # obj += 2
    print(sys.getrefcount(obj))  # obj += 1

p = Person()  # p = 1
log(p)  # p = 4

print(sys.getrefcount(obj))  # p = 2
複製代碼

對象在離開函數做用域時,會斷開和函數對象之間的引用,所以最後 p 的引用計數爲 2。測試

循環引用

簡單來講,當一個對象再也不使用時,應該被釋放,可是,當對象被刪除後仍然存在引用計數時,將沒法釋放該對象。spa

class Person:
    def __del__(self):
        print("Person({0}) 被釋放".format(id(self)))

class Dog:
    def __del__(self):
        print("Dog({0}) 被釋放".format(id(self)))

p = Person()  # p = 1
dog = Dog()  # dog = 1

# 循環引用
p.pet = dog  # dog = 2
dog.master = p  # p = 2

# 程序結束前 __del__() 不被調用
# 因爲循環引用,本質上沒法真正刪除 p, dog,只是在語法層面上刪除了它們。
del p, dog  # p, dog = 1, 1
複製代碼

在語法層面上,pdog 被刪除後就沒法再使用了,也沒法經過 pdog 的屬性 petmaster 來找到它們。 所以,將 pdog 稱之爲 可到達的引用,將 petmaster 稱爲 不可到達的引用。也就是說,將 pdog 刪除後,雖然 petmaster 所引用的 dogp 還在內存中,可是已經沒法經過正常手段來訪問他們了,pdog 對象將在內存中沒法被釋放掉。設計

當被 del 後的對象還存在引用計數時,經過 引用計數器機制 就沒法作到真正從內存中回收它們,因而就形成了,由循環引用引發的內存泄漏問題。3d

""" 錯誤!未定義 p, dog print(p) print(dog) """
複製代碼

垃圾回收機制

Python 由兩套內存管理機制並存,分別是 引用計數器機制垃圾回收機制。引用計數器機制性能優於垃圾回收機制,可是沒法回收循環引用。所以,垃圾回收機制的主要做用在於,從 經歷過引用計數器機制後 仍未被釋放的對象中,找到循環引用並釋放掉相關對象。code

垃圾回收的底層機制(如何找到循環引用?)orm

  1. 收集全部 容器對象 ( list , dict , tuple , customClass, ... ) ,經過一個雙向鏈表進行引用;
  2. 針對每個容器對象,經過一個變量 gc_refs 來記錄當前對應的引用計數;
  3. 對於每一個容器對象,找到它所引用的容器對象,並將這個容器對象的引用計數 -1;
  4. 通過步驟 3 後,若是一個容器對象的引用計數爲 0,就表明這個對象能夠被回收了,確定是 "循環引用" 才致使它活到如今的。

分代回收(如何提高查找循環引用的性能?)htm

若是程序中建立了不少個對象,而針對每個對象都要參與 檢測 過程,則會很是的耗費性能,基於這個問題,Python 提出了一個假設,那就是:越命大的對象越長壽。

假設一個對象被檢測 10 次都沒有把它釋放掉,就認定它必定很長壽,就減小對這個對象的 檢測頻率

分代檢測(基於假設設計出的一套檢測機制)

  1. 默認一個對象被建立出來後,屬於第 0 代;
  2. 若是經歷過這一代 垃圾回收 後,依然存活,則劃分到下一代;

垃圾回收的週期順序

  • 0 代 "垃圾回收" 必定次數後,觸發 0~1 代回收;
  • 1 代 "垃圾回收" 必定次數後,觸發 0~2 代回收。

關於分代回收機制,它主要的做用是能夠減小垃圾檢測的頻率。嚴格來講,除了它有這個機制限定外,還有一個限定它的條件,那就是,在 垃圾回收器 中,當 "新增的對象個數 - 銷燬的對象個數 = 規定閾值" 時纔會去檢測。

觸發垃圾回收

  1. 自動回收

    觸發條件是,開啓垃圾回收機制 ( 默認開啓 ),而且達到了垃圾回收的閾值。

    須要注意的是,觸發並非檢查全部的對象,而是分代回收。

  2. 手動回收 ( 默認0~2 )

    只需執行 gc.collect(n)n 能夠是 0~2,表示回收 0~n 代垃圾。

gc 模塊

gc 模塊能夠查看或修改 垃圾回收器 當中的一些信息。

import gc
複製代碼
  • gc.isenabled()

    判斷垃圾回收器機制是否開啓。

  • gc.enable()

    開啓垃圾回收器機制 ( 默認開啓 ) 。

  • gc.disable()

    關閉垃圾回收器機制。

  • gc.get_threshold()

    獲取觸發執行垃圾檢測閾值,返回值是一個元組 ( threshold, n1, n2 )

    • threshold

      就是觸執行發垃圾檢測的閾值,當 新增的對象個數 - 銷燬的對象個數 = threshold 時,執行一次垃圾檢測。

    • n1

      表示當 0 代垃圾檢測達到 n1 次時,觸發 0~1 代垃圾回收。

    • n2

      表示當 1 代垃圾檢測達到 n2 次時,觸發 1~2 代垃圾回收。

  • gc.set_threshold(1000, 15, 15)

    修改垃圾檢測頻率。通常狀況下,爲了程序性能,會把這些數值調大。

測試自動回收 1

import gc

# "建立對象的次數 - 銷燬對象的次數 = 2" 時,觸發自動回收。
gc.set_threshold(2, 10, 10)

class Person:
    def __del__(self):
        print(self, "被釋放")

class Dog:
    def __del__(self):
        print(self, "被釋放")

p = Person()  # p = 1
dog = Dog()  # dog = 1

# 循環引用
p.pet = dog  # dog = 2
dog.master = p  # p = 2

# 多建立一個 Person 類,目的是爲測試在刪除對象後,程序可以觸發自動回收。
p2 = Person()

# 程序結束前,不調用 __del__()。
del p
del dog
複製代碼

總共建立 3 個對象,銷燬了 1 個對象,3-1=2。理論上說,此時應該觸發自動回收,但直到程序結束以前,__del__() 函數都沒有被調用,這是爲何呢?

要解釋這個問題,首先就要了解,爲何垃圾檢測會存在 "新增的對象個數 - 銷燬的對象個數 = 規定閾值" 這樣一個限定條件。

這是由於,當對象遺留在內存中沒法被釋放時,緣由一般是對象建立多了而沒有被及時銷燬的緣由。

那麼根據這個結論,就能夠設定一個機制,當 "建立的對象" 多出 "被銷燬的對象" 大於或等於 "指定閾值" 時,再讓程序去檢測垃圾回收,不然不觸發檢測。

在銷燬一個對象時,表現的是,將減小一次達到指定閾值的條件,也就沒有必要再去檢測了。

因此嚴格來講,這個限定條件要改爲:在建立對象時,"新增的對象個數 - 銷燬的對象個數 = 規定閾值" 時 ,觸發垃圾檢測。

瞭解了這些以後,你就知道,爲何這裏對象沒法被釋放了。首先建立了 3 個對象,而後執行 del pdel dog,而在執行銷燬操做時,是不會觸發垃圾檢測的,所以對象不被釋放。

注意

此結論是我我的推測的,也有可能真是狀況並非這樣。我也是想了很久爲何不釋放對象,最終想到的一個比較合理的解釋。

測試自動回收 2

import gc
gc.set_threshold(2, 10, 10)

class Person:
    def __del__(self):
        print(self, "被釋放")

class Dog:
    def __del__(self):
        print(self, "被釋放")

p = Person()  # p = 1
dog = Dog()  # dog = 1

# 循環引用
p.pet = dog  # dog = 2
dog.master = p  # p = 2

# 嘗試在刪除 "可到達引用" 後,真實對象是否有被回收。
del p, dog

# 多建立一個 Person 類,目的是爲測試在刪除對象後,程序可以觸發自動回收。
p2 = Person()
print("p2 =", p2)

print("----------------------- 程序結束 -----------------------")

""" <__main__.Person object at 0x0000000002c28190> 被釋放 <__main__.Dog object at 0x0000000002cf33d0> 被釋放 p2 = <__main__.Person object at 0x0000000002cf3350> ----------------------- 程序結束 ----------------------- <__main__.Person object at 0x0000000002cf3350> 被釋放 """
複製代碼

總共建立 5 個對象,銷燬了 3 個對象,5-3=2,觸發自動檢測。此時發現 p , g 已被銷燬 ( 真實對象還在內存中 ),因而找到它們所引用的對象,將計數 -1,pdog 得以被釋放。

注意:是 pdog 先被釋放,p2 在程序結束後被釋放。

手動回收

import gc

class Person:
    def __del__(self):
        print(self, "被釋放")

class Dog:
    def __del__(self):
        print(self, "被釋放")

p = Person()  # p = 1
dog = Dog()  # dog = 1

# 循環引用
p.pet = dog  # dog = 2
dog.master = p  # p = 2

del p  # p = 1
del dog  # dog = 1

# 對程序執行垃圾檢測 (無關回收機制是否開啓),手動回收內存。
gc.collect()

# <__main__.Person object at 0x109cb0110> 被釋放
# <__main__.Dog object at 0x109cb0190> 被釋放
複製代碼

弱引用

import weakref
import sys

class Person:
    def __del__(self):
        print(self, "被釋放")

class Dog:
    def __del__(self):
        print(self, "被釋放")

p = Person()  # p = 1
dog = Dog()  # dog = 1

p.pet = dog  # dog = 2
# weakref.ref 不強引用指定對象 (即不增長引用計數)。
dog.master = weakref.ref(p)  # p = 1

# p 被徹底銷燬時,它所引用對象的計數 -1.
del p  # p = 0, dog = 1
del dog  # dog = 0

# <__main__.Person object at 0x109cb0110> 被釋放
# <__main__.Dog object at 0x109cb0190> 被釋放
複製代碼

爲證實一個對象被銷燬時,它所引用對象的計數是否 -1,特此作個實驗,來觀察 p 被銷燬時,它所指向的 dog 引用計數。

p.pet = dog  # dog = 2
dog.master = weakref.ref(p)  # p = 1

del p  # p = 0, dog = 1

""" 觀察 p 被銷燬時,它所引用的 dog 計數是否被 -1 sys.getrefcount 用於獲取一個對象的當前引用計數,返回值比實際值多 1。 """
print(sys.getrefcount(dog))  # 2

del dog  # dog = 0
複製代碼

p 被銷燬時,意味着在 p.pet = god 這條語句中,前面的 pp.pet 已經不存在了,只剩下 = dog ,前面空空如也,並不被任何對象所引用,所以 dog 的引用計數 -1。

而在強引用下,p 被銷燬時,dog 的引用計數不變。

p.pet = dog  # dog = 2
dog.master = p  # p = 2

del p  # p = 1, dog = 2
print(sys.getrefcount(dog))  # 3,實際值爲 2.
del dog  # dog = 1
複製代碼

要在一個集合中弱引用對象,使用 weakref.Weak...

# 弱所引用字典中的對象
# pets = weakref.WeakValueDictionary({"dog": d1, "cat": c1})
複製代碼

手動打破循環引用

class Person:
    def __del__(self):
        print(self, "被釋放")

class Dog:
    def __del__(self):
        print(self, "被釋放")

p = Person()  # p = 1
dog = Dog()  # dog = 1

p.pet = dog  # dog = 2
dog.master = p  # p = 2

""" 在刪除前手動打破循環引用 這意味着手動斷開 p.pet 與 dog 之間的引用, 當 dog 再也不被 p 引用時,計數天然 -1。 """
p.pet = None
del p  # p = 0, dog = 1
del dog  # dog = 0
複製代碼
相關文章
相關標籤/搜索