PHP的垃圾回收機制-回收週期

柳旦旦發表於2021-01-14

回收週期(Collecting Cycles)

傳統上,像以前的 php 用到的引用計數記憶體機制,無法處理迴圈的引用記憶體洩漏。然而 5.3.0 PHP 使用文章» 引用計數系統中的同步週期回收(Concurrent Cycle Collection in Reference Counted Systems)中的同步演算法,來處理這個記憶體洩漏問題。

對演算法的完全說明有點超出這部分內容的範圍,將只介紹其中基礎部分。首先,我們先要建立一些基本規則,如果一個引用計數增加,它將繼續被使用,當然就不再在垃圾中。如果引用計數減少到零,所在變數容器將被清除(free)。就是說,僅僅在引用計數減少到非零值時,才會產生垃圾週期(garbage cycle)。其次,在一個垃圾週期中,通過檢查引用計數是否減1,並且檢查哪些變數容器的引用次數是零,來發現哪部分是垃圾。

垃圾回收演算法

為避免不得不檢查所有引用計數可能減少的垃圾週期,這個演算法把所有可能根(possible roots 都是zval變數容器),放在根緩衝區(root buffer)中(用紫色來標記,稱為疑似垃圾),這樣可以同時確保每個可能的垃圾根(possible garbage root)在緩衝區中只出現一次。僅僅在根緩衝區滿了時,才對緩衝區內部所有不同的變數容器執行垃圾回收操作。看上圖的步驟 A。

在步驟 B 中,模擬刪除每個紫色變數。模擬刪除時可能將不是紫色的普通變數引用數減”1”,如果某個普通變數引用計數變成0了,就對這個普通變數再做一次模擬刪除。每個變數只能被模擬刪除一次,模擬刪除後標記為灰(原文說確保不會對同一個變數容器減兩次”1”,不對的吧)。

在步驟 C 中,模擬恢復每個紫色變數。恢復是有條件的,當變數的引用計數大於0時才對其做模擬恢復。同樣每個變數只能恢復一次,恢復後標記為黑,基本就是步驟 B 的逆運算。這樣剩下的一堆沒能恢復的就是該刪除的藍色節點了,在步驟 D 中遍歷出來真的刪除掉。

演算法中都是模擬刪除、模擬恢復、真的刪除,都使用簡單的遍歷即可(最典型的深搜遍歷)。複雜度為執行模擬操作的節點數正相關,不只是紫色的那些疑似垃圾變數。

現在,你已經對這個演算法有了基本瞭解,我們回頭來看這個如何與PHP整合。預設的,PHP的垃圾回收機制是開啟的,然後有個 php.ini 設定允許你修改它:zend.enable_gc

當垃圾回收機制開啟時,每當根快取區存滿時,就會執行上面描述的迴圈查詢演算法。根快取區有固定的大小,可存10,000個可能根,當然你可以通過修改PHP原始碼檔案Zend/zend_gc.c中的常量GC_ROOT_BUFFER_MAX_ENTRIES,然後重新編譯PHP,來修改這個10,000值。當垃圾回收機制關閉時,迴圈查詢演算法永不執行,然而,可能根將一直存在根緩衝區中,不管在配置中垃圾回收機制是否啟用。

當垃圾回收機制關閉時,如果根緩衝區存滿了可能根,更多的可能根顯然不會被記錄。那些沒被記錄的可能根,將不會被這個演算法來分析處理。如果他們是迴圈引用週期的一部分,將永不能被清除進而導致記憶體洩漏。

即使在垃圾回收機制不可用時,可能根也被記錄的原因是,相對於每次找到可能根後檢查垃圾回收機制是否開啟而言,記錄可能根的操作更快。不過垃圾回收和分析機制本身要耗不少時間。

除了修改配置zend.enable_gc,也能通過分別呼叫gc_enable()gc_disable()函式來開啟和關閉垃圾回收機制。呼叫這些函式,與修改配置項來開啟或關閉垃圾回收機制的效果是一樣的。即使在可能根緩衝區還沒滿時,也能強制執行週期回收。你能呼叫gc_collect_cycles()函式達到這個目的。這個函式將返回使用這個演算法回收的週期數。

允許開啟和關閉垃圾回收機制並且允許自主的初始化的原因,是由於你的應用程式的某部分可能是高時效性的。在這種情況下,你可能不想使用垃圾回收機制。當然,對你的應用程式的某部分關閉垃圾回收機制,是在冒著可能記憶體洩漏的風險,因為一些可能根也許存不進有限的根緩衝區。因此,就在你呼叫gc_disable()函式釋放記憶體之前,先呼叫gc_collect_cycles()函式可能比較明智。因為這將清除已存放在根緩衝區中的所有可能根,然後在垃圾回收機制被關閉時,可留下空緩衝區以有更多空間儲存可能根。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
專注細節,慢慢提升自己。✍️

相關文章