1. 程式人生 > 其它 >猴子補丁、垃圾回收機制

猴子補丁、垃圾回收機制

目錄

猴子補丁、垃圾回收機制

一 猴子補丁

1 什麼是猴子補丁

	屬性在執行時的動態替換,叫做猴子補丁(Monkey  Patch) 。

	猴子補丁的核心就是用自己的程式碼替換所用模組的原始碼。

2 猴子補丁的功能(一切皆物件)

	擁有在模組執行時替換的功能,例如:一個函式物件賦值給另一個函式物件(把函式原本的執行的功能給替換了) 。

3 monkey patch 的應用場景

	如果我們的程式中已經基於json模組編寫了大量程式碼了,發現有一個模組ujson比他效能更高,但用法一樣,我們肯定不會想把所有的程式碼都換成ujson.dumps或者ujson.loads,那我們可能會想到這麼做:import ujson as json,但是這麼做需要將每一個檔案都重新匯入一下,維護成本依然很高,此時我們就可以用到猴子補丁了。
import json 
import ujson

def monkey_patch_json():
    json.__name__ = 'ujson'
    json.dumps = ujson.dumps
    json.loads = ujson.loads
   
monkey_patch_json() # 之所以在入口處加,是因為模組在匯入一次後,後續的匯入便直接引用第一次的成果

#其實這種場景也比較多, 比如我們引用團隊通用庫裡的一個模組, 又想豐富模組的功能, 除了繼承之外也可以考慮用Monkey Patch.採用猴子補丁之後,如果發現ujson不符合預期,那也可以快速撤掉補丁。
# **缺點:**
	可能會汙染原始碼。

二 垃圾回收機制

1 什麼是垃圾回收機制

	垃圾回收機制(簡稱GC) 是Python直譯器自帶一種機,專門用來回收不可用的變數值所佔用的記憶體空間。

2 為何要用垃圾回收機制

	程式執行過程中會申請大量的記憶體空間,而對於一些無用的記憶體空間如果不及時清理的話會導致記憶體使用殆盡(記憶體溢位) ,導致程式崩潰,因此管理記憶體是一件重要且繁雜的事情,而python直譯器自帶的垃圾回收機制把程式設計師從繁雜的記憶體管理中解放出來。

3 理解GC原理的基礎知識

3.1 堆區與棧區

​ 定義變數的時候,變數名與變數值都是需要儲存的,分別對應記憶體中的兩塊區域:堆區與棧區。

# 1、變數名與值記憶體地址的關聯關係存放於棧區.

# 2、變數值存放於堆區,記憶體管理回收的則是堆區的內容.

3.2 直接引用與間接引用

	直接引用指的是從棧區出發直接引用到的記憶體地址。	間接引用指的是從棧區出發引用到堆區後,再通過進一步引用才能到達的記憶體地址。

4 垃圾回收機制原理分析

	Python的GC模組主要運用了“引用計數”(reference counting) 來跟蹤和回收垃圾。在引用計數的基礎上,還可以通過“標記-清除”(mark and sweep) 解決容器物件可能產生的迴圈引用的問題,並且通過“分代回收”(generation collection) 以空間換取時間的方式來進一步提高垃圾回收的效率。

4.1 引用計數

​ 引用計數就是:變數值被變數名關聯的次數(被間接引用也會增加引用計數) 。

4.2 引用計數的問題一:迴圈引用

​ 引用計數機制存在著一個致命的弱點,即迴圈引用(也稱交叉引用) 。

# 如下我們定義了兩個列表,簡稱列表1與列表2,變數名l1指向列表1,變數名l2指向列表2>>> l1=['xxx']  # 列表1被引用一次,列表1的引用計數變為1   >>> l2=['yyy']  # 列表2被引用一次,列表2的引用計數變為1   >>> l1.append(l2)             # 把列表2追加到l1中作為第二個元素,列表2的引用計數變為2>>> l2.append(l1)             # 把列表1追加到l2中作為第二個元素,列表1的引用計數變為2# l1與l2之間有相互引用# l1 = ['xxx'的記憶體地址,列表2的記憶體地址]# l2 = ['yyy'的記憶體地址,列表1的記憶體地址]>>> l1['xxx', ['yyy', [...]]]>>> l2['yyy', ['xxx', [...]]]>>> l1[1][1][0]'xxx'

​ 迴圈引用會導致:值不再被任何名字關聯,但是值的引用計數並不會為0,應該被回收但不能被回收。

>>> del l1 # 列表1的引用計數減1,列表1的引用計數變為1>>> del l2 # 列表2的引用計數減1,列表2的引用計數變為1

​ 此時,只剩下列表1與列表2之間的相互引用:

	但此時兩個列表的引用計數均不為0,但兩個列表不再被任何其他物件關聯,沒有任何人可以再引用到它們,所以它倆佔用記憶體空間應該被回收,但由於相互引用的存在,每一個物件的引用計數都不為0,因此這些物件所佔用的記憶體永遠不會被釋放,所以迴圈引用是致命的,這與手動進行記憶體管理所產生的記憶體洩露毫無區別。 所以Python引入了“標記-清除” 與“分代回收”來分別解決引用計數的迴圈引用與效率低的問題。

4.3 問題一解決方案:標記--清除

	容器物件(比如:list,set,dict,class,instance) 都可以包含對其他物件的引用,所以都可能產生迴圈引用。而“標記-清除”計數就是為了解決迴圈引用的問題。	標記/清除演算法的做法是當應用程式可用的記憶體空間被耗盡的時,就會停止整個程式,然後進行兩項工作,第一項則是標記,第二項則是清除.
#1、標記通俗地講就是:棧區相當於“根”,凡是從根出發可以訪達(直接或間接引用) 的,都稱之為“有根之人”,有根之人當活,無根之人當死。具體地:標記的過程其實就是,遍歷所有的GC Roots物件(棧區中的所有內容或者執行緒都可以作為GC Roots物件) ,然後將所有GC Roots的物件可以直接或間接訪問到的物件標記為存活的物件,其餘的均為非存活物件,應該被清除。#2、清除清除的過程將遍歷堆中所有的物件,將沒有標記的物件全部清除掉。

​ 基於上例的迴圈引用,當我們同時刪除l1與l2時,會清理到棧區中l1與l2的內容以及直接引用關係。

​ 這樣在啟用標記清除演算法時,從棧區出發,沒有任何一條直接或間接引用可以訪達l1與l2,即l1與l2成了“無根之人”,於是l1與l2都沒有被標記為存活,二者會被清理掉,這樣就解決了迴圈引用帶來的記憶體洩漏問題。

4.4 引用計數的問題二:效率問題

​ 基於引用計數的回收機制,每次回收記憶體,都需要把所有物件的引用計數都遍歷一遍,這是非常消耗時間的,於是引入了分代回收來提高回收效率,分代回收採用的是用“空間換時間”的策略。

4.5 問題二解決方案:分代--回收

分代:

​ 分代回收的核心思想是:在歷經多次掃描的情況下,都沒有被回收的變數,gc機制就會認為,該變數是常用變數,gc對其掃描的頻率會降低,具體實現原理如下:

分代指的是根據存活時間來為變數劃分不同等級(也就是不同的代) 新定義的變數,放到新生代這個等級中,假設每隔1分鐘掃描新生代一次,如果發現變數依然被引用,那麼該物件的權重(權重本質就是個整數) 加一,當變數的權重大於某個設定得值(假設為3) ,會將它移動到更高一級的青春代,青春代的gc掃描的頻率低於新生代(掃描時間間隔更長) ,假設5分鐘掃描青春代一次,這樣每次gc需要掃描的變數的總個數就變少了,節省了掃描的總時間,接下來,青春代中的物件,也會以同樣的方式被移動到老年代中。也就是等級(代) 越高,被垃圾回收機制掃描的頻率越低。

回收:

​ 回收依然是使用引用計數作為回收的依據

![](

​ 雖然分代回收可以起到提升效率的效果,但也存在一定的缺點:

#例如一個變數剛剛從新生代移入青春代,該變數的繫結關係就解除了,該變數應該被回收,但青春代的掃描頻率低於新生代,這就到導致了應該被回收的垃圾沒有得到及時地清理。沒有十全十美的方案:毫無疑問,如果沒有分代回收,即引用計數機制一直不停地對所有變數進行全體掃描,可以更及時地清理掉垃圾佔用的記憶體,但這種一直不停地對所有變數進行全體掃描的方式效率極低,所以我們只能將二者中和。綜上垃圾回收機制是在清理垃圾&釋放記憶體的大背景下,允許分代回收以極小部分垃圾不會被及時釋放為代價,以此換取引用計數整體掃描頻率的降低,從而提升其效能,這是一種以空間換時間的解決方案目錄