1. 程式人生 > >python中的記憶體管理與分析以及垃圾回收機制

python中的記憶體管理與分析以及垃圾回收機制

1.記憶體分析和處理

程式的執行離不開對記憶體的操作,一個軟體要執行,需要將資料載入到記憶體中,通過CPU進行記憶體資料的讀寫,完成資料的運算。

1.1不可變資料型別VS可變資料型別

python中根據資料是否可以進行修改提供了兩種不同的資料型別 ⚫ 不可變資料型別:一般基本資料型別都是不可變資料型別 ⚫ 可變資料型別:一般組合資料型別或者自定義資料類都是可變資料型別 怎麼區分可變和不可變?為什麼要有這樣的規則? PYTHON 中的一切都是物件,可以通過 id()函式查詢物件在記憶體中的地址資料 可變資料型別是在定義了資料之後,修改變數的資料,記憶體地址不會發生變化 不可變資料型別是在定義了資料之後,修改變數的資料,變數不會修改原來記憶體地址的資料

而是會指向新的地址,原有的資料保留,這樣更加方便程式中基本資料的利用率

def chg_data_1(x):
    x = 12
    print("method: {}".format(x))


def chg_data_2(y):
    y.append("hello")
    print("method: {}".format(y))


if __name__ == '__main__':
    a = 10
    chg_data_1(a)
    print(a)

    b = [1, 2, 3]
    chg_data_2(b)  # 實際引數傳遞可變型別

    print(b)  # 這裡的 b是多少 ?

1.2程式碼和程式碼塊

PYTHON 中的最小執行單元是程式碼塊,程式碼塊的最小單元是一行程式碼 如 print(‘hello,world’) 在實際開發中python中有兩種操作模式

  • 互動模式 在互動模式下,每行命令是一個獨立執行的程式碼塊,每個程式碼塊執行會獨立申請一次記憶體,在操作過程中互動模式沒有退出的情況下遵循python官方操作標準 如:對基本資料型別進行了基本優化操作,將一定範圍內的資料儲存在常量區以提升效能 在這裡插入圖片描述
  • IDE開發模式 在 IDE 開發模式下,程式碼封裝在模組中,通過python 命令執行模組時,模組整體作為一個程式碼塊向系統申請記憶體並執行程式,執行過程中對於基本資料型別進行快取優化操 作,通過 pycharm 工具執行上述程式碼測試: 在這裡插入圖片描述

1.3程式記憶體程式碼檢測

python中對於記憶體的操作,社群開發一款比較強大的專門用於檢測程式碼記憶體使用率,用於專案程式碼調優的模組memory_profile是一個使用較為簡單,並且視覺化比較直觀的工具模組 通過pip install memory_profile安裝即可使用

from memory_profiler import profile 

@profile
def chg_data_1(x):
    x = 12
    print("method: {}".format(x))

@profile
def chg_data_2(y):
    y.append("hello")
    print("method: {}".format(y))


if __name__ == '__main__':
    a = 10
    chg_data_1(a)
    print(a)

    b = [1, 2, 3]
    chg_data_2(b)  
    print(b)

1.4操作符號:is和==的使用

PYTHON 提供了物件操作符號 is 和內容操作符號==,用於判斷對 象和物件中的值的情況。 A is B:判斷物件 A 和物件 B 是否同一個記憶體地址,即是否同一個物件 。 A == B:判斷 A 中的內容是否和 B 中的內容一致 。 不論是基本型別的資料,還是內容複雜的物件,都可以通過物件判斷符號 is 和內容判斷操 作符號==來進行確定 不可變資料型別的資料判斷 在這裡插入圖片描述 組合資料型別的資料判斷 建立的每個組合資料型別的物件都是獨立的,如下面的程式碼中的 a 和 b 變數中分別存放了 兩個不同的列表型別的物件,所以 is 判斷是 False,但是值又是相同的所以 == 判斷 True 在這裡插入圖片描述

引用資料型別的操作: 自定義資料型別,變數中存放的是物件在記憶體中的地址自定義型別的物件,每次建立同樣也是在堆記憶體中單獨建立的物件,所以 is 判斷 False 但是為什麼我們建立的兩個 name 屬性都為 tom 的物件,通過== 判斷還是 Flase 呢?哪是 因為建立的物件在通過 == 判斷時,會自動呼叫父類的__eq__()函式進行判斷,預設情況下 建立的物件內部變數,使用的是物件自己的記憶體空間,所以==判斷是 False 。 在這裡插入圖片描述

1.5引用、淺拷貝、深拷貝

物件的建立,依賴與申請的記憶體空間資料的載入,物件在記憶體中建立的過程依賴於三部分記憶體處理:

  1. 物件分配記憶體地址
  2. 引用變數分配記憶體地址
  3. 物件和引用變數之間的關聯
from memory_profiler import profile


class Person:
    def __init__(self, name, age, gender):
        self.name = name
        self.age = age
        self.gender = gender


@profile(precision=10)
def main():
    p1 = Person('lkk', 20, '男')
    print(p1)
    p2 = Person('lkk', 20, '男')
    print(p2)


if __name__ == '__main__':
    main()

執行結果截圖: 在這裡插入圖片描述 記憶體分配圖解 在這裡插入圖片描述 由於物件的建立,是將堆記憶體中建立的物件的地址臨時儲存在棧記憶體中的變數中,那麼在程式中如果要在多個地方使用一個物件資料時應該怎麼辦呢?一般想到的都是將物件像檔案一樣複製一份不就好了。 PYTHON 中對於這樣的情況,有三種不同的操作方式 ⚫ 如果程式中多個不同的地方都要使用同一個物件,通過物件的引用賦值,將同一個物件賦值給多個變數。 ⚫ 如果程式中多個不同的地方都要使用相同的物件資料,通過物件的拷貝完成資料的淺拷貝即可,物件中的包含的資料要求必須統一 。 ⚫ 如果程式中多個不同的地方使用相同的而且獨立的物件資料,通過物件的深拷貝將物件的資料完整複製成獨立的另一份即可 。

1.5.1物件的引用賦值

物件的引用賦值,可以將 物件的記憶體地址同時賦值給多個變數,這多個變數中存放的都是同一個物件的引用地址,如果通過一個變數修改了物件的內容,那麼其他變數指向的物件內容也會同步發生改變。 物件引用變數的賦值,並不是物件的複製或者備份,而僅僅是將物件的地址儲存在多個變數中方便程式操作。 在這裡插入圖片描述 注意:PYTHON 中所謂物件的引用賦值,針對的是可變型別,不論是組合資料型別或者自定 義Class型別,都具備引用賦值的操作;但是不適合不可變型別,不可變型別的引用賦值操作有只讀不寫的特徵,一旦通過變數重新賦值就會重新指向新的引用物件。

1.5.2物件的淺拷貝

對於程式中物件的拷貝操作,除了引用賦值操作可以完成同一個人物件在程式不同位置的操作,這裡的淺拷貝更是一種物件的臨時備份,淺拷貝的核心機制主要是賦值物件內部資料的引用。 python內建模組copy提供了一個人copy函式可以完成。 在這裡插入圖片描述

1.5.3物件的深拷貝

和物件的淺拷貝不同,物件的深拷貝,是物件資料的直接拷貝,而不是簡單的引用拷貝,主要是通過 PYTHON 內建標準模組 copy 提供的 deepcopy 函式可以完成物件深拷貝。 在這裡插入圖片描述

2.垃圾回收機制(GC/gc)

Garbage collection ----->垃圾回收機制 在 PYTHON 中的垃圾回收機制 主要是以引用計數為主要手段 以標記清除和隔代回收機制作為輔助操作手段 完成對記憶體中無效資料的自動管理操作的。

2.1垃圾回收機制的意義

垃圾回收機制(Garbage Collection:GC)基本是所有高階語言的標準配置之一了 在一定程度上,能優化程式語言的資料處理效率和提高程式設計軟體開發軟體的安全效能。 ###2.1.1引用計數 引用計數[Reference Counting:RC]是 PYTHON 中的垃圾回收機制的核心操作演算法 該演算法最早是 George E.Collins 在 1960 年首次提出的,並在大部分高階語言中沿用 至今,是很多高階語言的垃圾回收核心演算法之一。 (1) 什麼是引用計數 引用計數演算法的核心思想是:當一個物件被建立或者拷貝時,引用計數就會+1,當這個物件的多個引用變數,被銷燬一個時該物件的引用計數就會-1,如果一個物件的引用計數為0則表示該物件已經不被引用,就可以讓垃圾回收機制進行清除並釋放該物件佔有的記憶體空間了。

引用計數演算法的優點是:操作簡單,實時效能優秀,能在最短的時間獲得並運算物件引用數

引用計數演算法的缺點是:為了維護每個物件的引用計數操作演算法,PYTHON 必須提供和物件對等的記憶體消耗來維護引用計數,這樣就在無形中增加了記憶體負擔;同時引用計數對於迴圈 應用/物件之間的互相引用,是無法進行引用計數操作的,所以就會造成常駐記憶體的情況。 PYTHON 是一個面向物件的弱型別語言,所有的物件都是直接或者間接繼承自 object 類 型,object 型別的核心其實就是一個結構體物件 在這個結構體中,ob_refcnt 就是物件的引用計數,當物件被建立或者拷貝時該計數就會 增加+1,當物件的引用變數被刪除時,該計數就會減少-1,當引用計數為 0 時,物件資料 就會被回收釋放了。在 python 中,可以通過 sys.getrefcount()來獲取一個物件的引 用計數 在這裡插入圖片描述

2.1.2標記清除

python中的標記—清除機制主要是針對可能產生迴圈引用的物件進行的檢測機制。 在python中的基本不可變型別如PyIntObject,PyStringObject等物件的內部不會內聚其他物件的引用,所以不會產生迴圈引用,一般情況下迴圈引用總是發生在其他可變物件的內部屬性中,如list dict class 等等 的使得該方法消耗的資源和程式中的可變物件的數量息息相關 標記清除演算法核心思想 :首先找到python中的一批根節點物件,如object物件,通過根節點物件可以找到他們指向的子節點物件,如果搜尋過程中有這個指向是從上往下的指向,表示這個物件是可達的,否則該物件是不可達的,可達部分的物件在程式中需要保留下來,不可達部分的物件在程式中是不需要保留的。

2…2.3分代回收

PYTHON 中的分代回收機制,是一種通過空間換取時間效率的做法,PYTHON 內部處理機制 定義了三個不同的連結串列資料結構[第零代(年輕代),第 1 代(中年代),第 2 代(老年代)] PYTHON 為了提高程式執行效率,將垃圾回收機制進行了閾值限定,0 代連結串列中的垃圾回收 機制執行最為密集,其次是 1 代,最後是 2 代; PYTHON 定義的這三個連結串列,主要是針對我們在程式中建立的物件,首先會新增到 0 代連結串列 在這裡插入圖片描述 隨後 0 代連結串列數量達到一定的閾值之後,觸發 GC 演算法機制,對 0 代物件進行符合規則的引 用計數運算,避免出現物件的延遲或者過早的釋放 在這裡插入圖片描述 最終,觸發 GC 機制將已經沒有引用指向的物件進行回收,並將有引用繼續 指向的物件移動到第 1 代物件連結串列中;第 1 代物件連結串列的物件,就是比第 0 代物件連結串列中的物件可能存活更久的物件,GC 閾值更大檢測頻率更慢,以提 高程式執行效率。 在這裡插入圖片描述 以此類推直到一部分物件存活在第 2 代物件連結串列中,物件週期較長的可能跟程式的生命周 期一樣了。

備註:弱代假說:程式中年輕的物件往往死的更快,年老的物件往往存活更久

2.3垃圾回收處理

PYTHON 中的垃圾回收機制有了一定的瞭解之後,我們針對垃圾回收機制的操作通過程式碼進 行測試 PYTHON 中的 gc 模組提供了垃圾回收處理的各項功能機制,必須 import gc 才能使用

gc.set_debug(flags):設定 gc 的 debug 日誌,一般為 gc.DEBUG_LEAK gc.collect([generation]):顯式進行垃圾回收處理,可以輸入引數~引數表示回收的 物件代數,0 表示只檢查第 0 代物件,1 表示檢查第 0、1 代物件,2 表示檢查 0、1、2 代 獨物件,如果不傳遞引數,執行 FULL COLLECT,也就是預設傳遞 2 gc.set_threshold(threshold0 [, threshold2 [, threshold3]]):設定執行 垃圾回收機制的頻率 gc.get_count():獲取程式物件引用的計數器 gc.get_threshold():獲取程式自動執行 GC 的引用計數閾值

以上是 PYTHON 中垃圾回收機制的基本操作,在程式開發過程中需要注意: ⚫ 專案程式碼中儘量避免迴圈引用 ⚫ 引入 gc 模組,啟用 gc 模組自動清理迴圈引用物件的機制 ⚫ 將需要長期使用的物件集中管理,減少 GC 資源消耗 ⚫ gc 模組處理不了重寫__del__方法導致的迴圈引用,如果一定要新增該方法,需要顯 式呼叫 gc 模組的 garbage 中物件的__del__方法進行處理