python屬於動態語言,我們可以隨意的創建和銷毀變量,如果頻繁的創建和銷毀則會浪費cpu,那么python內部是如何優化的呢?
python和其他很多高級語言一樣,都自帶垃圾回收機制,不用我們去維護,也避免了出現內存泄漏,懸空指針等bug,那么python內部如何進行垃圾回收的呢?
python的垃圾回收,我們用gc模塊去開啟或者關閉它,那么gc模塊又是什么呢?
python的優化機制

python垃圾回收之引用計數

關於循環引用進行一個測試
import gc # python的一個垃圾回收模塊,garbage collection class A(object): def __init__(self): pass def foo(): while True: a1 = A() a2 = A() a1.t = a2 a2.t = a1 del a1 del a2 gc.disable() foo() ''' 當我們使用gc.disable(),就意味着我們把python的垃圾回收機制給關閉了,那么一些垃圾便不會被自動回收。 首先我們創建了兩個實例對象a1和a2,然后在a1的內部創建一個屬性指向a2,在a2的內部創建一個屬性指向a1 那么a1和a2的引用計數都為2,即使del a1,del a2,它們的引用計數還是為1,因此仍然會待在內存里不會被回收 當我們進行第二輪循環的時候,會繼續再內存中創建一份a1和a2,不斷地循環會發現程序所使用的內存也來越大,直至溢出崩潰 '''

探討一下ruby和python的垃圾回收機制
ruby和python均屬於腳本語言,它們是非常相似的,比方說定義兩個類
ruby的對象分配
當我們執行上面的Node.new(1)時,ruby在內部給我們做了什么?ruby是如何創建新的對象的呢?其實ruby做的很少,實際上早在代碼執行之前,ruby就創建了成百上千個對象(類似於python的小整數對象池),並把它們串在鏈表上,叫做可用鏈表
因此在創建對象的時候直接拿來用就可以了。

白色的小方格相當於標着一個“未使用預創建對象”,當我們調用Node.new方法時,ruby直接取出一個給我們使用即可

可以簡單地理解,黑色格子是當前使用的對象,白色格子是未使用對象。當我們創建了一個“ABC”,放在了第一個格子的位置,讓n1這個變量指向它。
由於這是一個鏈表,鏈表上的對象一般保存着下一個對象的內存指針,因此我們只需要找到一個便可以順藤摸瓜將剩下的全給找出來,由於已經被使用,那么“箭頭”也被切斷了,剩下的白色格子在一個鏈表上,保存的是可用的內存
如果我們再次調用Node.new,ruby將繼續遞給我們一個對象。

因此隨着我們不斷地創建,ruby不斷的從可用鏈表里取預創建對象給我們,從而也就導致了可用鏈表會越來越短

注意圖像,我為n1不止賦了一次值,賦完值之后,又給n1賦了新的值,讓n1指向了新的內存空間,那么就會有垃圾產生。
ruby的標記--清除
當我們把可用空間全部用完了,可用鏈表上變得空空如也

可以看到所有格子都變灰了,沒有白格子了。那么ruby是如何解決這一問題的呢?
ruby使用的是標記--清除,首先ruby會把程序停下來,然后輪詢所有的指針,變量和代碼產生的別的引用對象以及其他值,同時ruby通過自身的虛擬機遍歷內部指針,只要對象被引用,那么便將其標記(mark)出來。

只要沒有被標記的,那么便是垃圾。

ruby將這些沒有被標記的垃圾進行回收,並把它們送回到可用鏈表當中。
由於內部發生的很快,ruby不會將對象到處拷貝,而是通過調整內部的指針,將其指向一個新鏈表的方式,來將垃圾對象歸位到可用鏈表當中的。
python的對象分配
我們了解了ruby是預先創建對象並將它們放在可用鏈表當中,那么python又是怎樣的呢?
在為新對象和變量分配內存方面,ruby和python是不同的
我們用python來創一個Node對象

與ruby不同,python在創建對象時立即向操作系統申請內存(實際上python實現了一套自己的內存分配系統,在操作系統堆之上提供了一個抽象層,這方面的內容以后再說)
當我們創建第二個對象時,繼續向操作系統申請內存。

給人感覺就像是,ruby在內存的申請和分配上先花了較長時間,接下來創建對象的時候效率高
python一開始並沒有花費太長的時間,而是在創建的時候才會申請,相當於把時間均攤了。
當我們有三個對象時

在創建對象時,python總是在對象的C結構體里保存一個整數,就是我們說的引用數,初始值為1
但是如果我們讓n1指向新的對象

那么python變把“ABC”的引用數設置為0了,因此也就變成了垃圾。此時python內部不斷運轉的垃圾回收機制便閃亮登場了,當引用數變為0,python會立即將其釋放,把內存空間交還給操作系統

因此ruby和python關於對象創建和垃圾回收的差異就是,ruby是一開始創建一大片,一大片都用完了再回收;python是用的時候才創建,用完了立刻回收。
可以理解為ruby是房間臟了不要緊,只要還能住人,然而當房間已經臟的沒法住人了,會進行一次大掃除。python是只要臟了就會立刻進行打掃。
話說,ruby貌似是日本人發明的吧,舉房間的例子貌似有點乖乖的,與我印象中的日本不太一樣(*^__^*,話說如果大家以后有時間,推薦去一下日本(鄉下),很美)
python垃圾回收之隔代回收
之前提到過循環引用
這種情況,python的引用計數是無法解決的,因此另一種垃圾清理方式便出現了,隔代回收
python在創建對象之前,也會創建一個鏈表,零代鏈表,只不過這個鏈表是空的。每當你創建一個對象,python便會將其加入到零代鏈表

當我們再創建一個對象時,python同樣會將其加入到鏈表當中。

接下來檢測循環引用
隨后python會遍歷零代鏈表上的每一個對象,檢測鏈表中每個循環引用的對象,根據規則減去其引用計數。在這個過程中,python會一個接一個的統計內部對象的引用數量以防過早地釋放對象。

從圖中可以看到,有三個其他的對象同時存在於零代鏈表中,藍色箭頭表示一些對象正在被零代鏈表之外的某些對象引用着。python還存在一代和二代鏈表,這些對象有着更高的引用計數,因為它們正在被其他指針所指向着
如何處理零代鏈表:
從圖中可以看到,"ABC","DEF"的引用計數為1,但是已經沒有任何變量向它們了,因此python在達到一定條件(這里的條件后面再說)時,會將零代鏈表的具有循環引用的對象(python解釋器不是上來就減去一,而是先判斷哪些對象是被循環引用的)的引用計數統統減去1
如果引用計數為零,那么會將其作為垃圾清理掉,而那些沒有被清理掉的,沒有循環引用的(引用計數減一,可以看作忽視掉了一個循環引用)對象會被移動到一代鏈表中

同理,python還有二代鏈表。一代鏈表存放的是那些在零代鏈表中被清理完之后還活着的,二代鏈表存放的是那些在一代鏈表中被清理之后還活着的。為什么會有三條鏈子,而不是一條。
因為python在清理鏈子的時候,是把鏈子上的所有對象進行一次清理,如果已經清理一次感覺很穩定了,那么沒有必要再清理了,因此將其移動到新的鏈子上。
因此python隔代回收的核心就是:對鏈子上的那些明明沒有被引用但引用計數卻不是零的對象進行引用計數減去一,看看你是不是垃圾。如果被引用多次減去一之后仍不為零,那么會在零代鏈表當中繼續被清理,直至引用計數為零。因為如果沒有變量指向它,或者作為函數的參數,列表的元素等等,那么它就始終是零代鏈表中被清理的對象。當零代鏈表被清理達到一定次數時(次數以及上面的條件在gc模塊中會說),會清理一代鏈表。一代鏈表被清理達到一定次數時,會清理二代鏈表。
因此清理的頻率最高的是零代鏈表,其次是一代鏈表,再是二代鏈表
因此關於python的垃圾回收用一句話總結就是:引用計數為主,隔代回收為輔,來完成垃圾回收。ruby則是標記清除
gc模塊

再來看看這段代碼,明明有垃圾回收,為什么程序的內存消耗還會不斷地增加呢?因為在第19行,我們將gc(垃圾回收)的功能給關閉了
gc還可以顯式的開啟

因此這段程序也不會導致程序占用內存不斷增加的情況產生
此外還可以通過gc.garbage,查看清理了哪些垃圾
不過gc一般默認是開啟的,我們不用去關心它們。
話說還記的上面說的當達到一定條件清理零代鏈表,一代鏈表,二代鏈表嗎?這個所謂的條件是什么呢?
在gc模塊中有這樣一個函數

這個元祖中有三個值,而且是不變的。第一個值700表示的是,當新創建的對象減去已經釋放的對象超過700的時候,會清理零代鏈表
第二個值10表示的是,當零代鏈表清理10次的時候,會清理一次一代鏈表。在清理一代鏈表的時候,會順便清理一次零代鏈表
第三個值10表示的是,當一代鏈表清理10次的時候,會清理一次二代鏈表。在清理二代鏈表的時候,會順便清理一次零代鏈表和一代鏈表
也可以自己設置這個gc隔代回收觸發條件的閾值

我們也可以查看當前的值是否達到了閾值

再來總結一下關於Python的引用計數
導致引用計數加一的情況:
對象被創建,a=1
對象被引用,b=a
對象被作為參數傳到一個函數中,func(a)
對象作為列表,元祖等等的一個元素,li=[a,a]
導致引用計數減一的情況:
對象的別名被顯式的銷毀,del a
對象的引用指向了新的對象,a=2
對象離開了它的作用域,比如函數的局部變量,在函數執行完畢的時候,也會被銷毀。(全局變量不會)
對象所在的容器被銷毀,或着從容器中刪除
查看一個對象的引用計數:
通過sys模塊下的一個函數

不過這里為什么是2,難道不是1嗎?這是因為我們把a扔進了sys.getrefcount()這個函數里,從而導致引用計數加1
可以看一下源碼

源碼的注釋里也說了,返回一個對象的引用計數,但是這個數通常比你所預想的要多1,這是因為它包含了作為參數傳給getrefcount()函數的引用。

關於gc,還有一個重要的一點。
gc進行清理對象的時候,本質上調用了對象內部的析構函數__del__,但是如果我們重寫了,那么它就不會執行自己的,或者父類的,那么對象也不會被清理掉。因此當我們在定義一個類,要重寫析構函數時,當我們寫完我們的代碼時,最好讓它再執行父類的析構方法。
這也算是gc的一個bug,不過這個bug只在python2中才會有,在我用的python3.6中不會出現這個bug,因此如果不是用python2的小伙伴的話,就不用擔心這個問題啦·········
以上就是關於python的垃圾回收的一些理解··············


