前言
大多數編程語言都會有自身的垃圾回收機制,php也不例外。經常聽很多人說gc,也就是垃圾回收器,全程為Garbage Collection。
在php5.3之前,是不包括垃圾回收機制的,也沒有專門的垃圾回收器,實現垃圾回收就是簡單判斷一下變量的zval的refcount是否為0,是的話就釋放。
但是如果這么簡單的判斷垃圾回收的話,很容易引起程序過程中內存溢出。如果存在"自身指向自身"的情況的話,那么變量將無法回收早成內存泄露,所以從php5.3開始就出現了專門負責清理垃圾數據防止內存泄露的垃圾回收器。
引用計數的基本知識
我們要了解GC,那么首先要了解引起垃圾回收的基數是什么。
在php中,每個變量存在一個叫“zval”的變量容器中。一個zval變量容器,除了包含變量的類型和值,還包括另外兩個字節的額外信息。第一個是"is_ref"。第二個是"refcount"。
is_ref是一個布爾類型的值,用來標示這個變量是否屬於引用集合。通過這個字節,php引擎才能把普通變量和引用變量區分開來,由於php允許用戶通過"&"來使用自定義的引用,所以zval中還有一個內部引用計數機制,來進行優化內存。
refcount用來表示這個zval變量容器的變量的個數。所有符號存在一個符號表當中,每個符號都有作用域。
通俗的講:
- refcount就是多少個變量是一樣的用了相同的值,那么refcount就是這個值
- is_ref就是當有變量用了&的形式進行賦值,那么is_ref的值就會增加
減少引用計數
復合類型
當變量的類型為array或object這樣的復合類型時,array和object類型的變量把他們的成員或屬性存在自己的符號表中。
添加一個已經存在的元素到數組中:
從數組中刪除一個元素:
將數組作為一個元素添加給自身:
我們可以看到數組a同時也是這個數組第二個元素,指向的變量容器中refcount的值為2,上面輸出的“...”說明發生了遞歸操作,意味着"..."指向原始數組。
盡管不再有某個作用域中的任何符號指向這個結構(就是變量容器),由於數組元素“1”仍然指向數組本身,所以這個容器不能被清除 。因為沒有另外的符號指向它,用戶沒有辦法清除這個結構,結果就會導致內存泄漏。
垃圾回收周期
在5.3之前的版本中,php無法處理循環的引用內存泄露。但是自5.3之后php使用引用計數系統中同步周期回收的同步算法,僅處理這個內存泄露問題。
基本思想是如果一個引用計數增加那么將繼續被使用,當然就不再是垃圾。如果引用計數減少到零,所在變量容器將被清除。那么也就是說只有在引用計數減少到非零值時,才會產生垃圾周期。在一個垃圾周期中通過檢查引用計數是否減1,並且檢查哪些變量容器的引用次數為零,來發現哪些是垃圾。
我們就拿這張圖舉例(來自php官網)。為了避免不得不檢查所有引用計數可能減少的垃圾周期,同步算法將所有可能根放在了根緩沖區(root buffer)中(在圖中用紫色來標記,稱為疑似垃圾),這樣可以同時確保每個可能的垃圾根在緩沖區中只出現一次。僅當根緩沖區滿了時,才對緩沖區中所有不同的變量容器執行垃圾回收操作,在圖中體現為步驟A。
在步驟B中,模擬刪除每個紫色的變量。模擬刪除時可能將不是紫色的不同變量引用數減1,如果某個普通變量引用計數變成0時,就對這個普通變量在做一次模擬刪除。每個變量只能被模擬刪除一次,模擬刪除后標記為灰色。
在步驟C中,模擬恢復每個紫色變量。當然這個恢復是有條件的,當變量的引用計數大於0時才對其做模擬恢復。同樣的每個變量只能恢復一次,恢復后標記為黑色,這樣生下一對沒能恢復的就是該刪除的藍色節點了,在步驟D中遍歷出來真正的刪除掉。
在php中垃圾回收機制默認是打開的,在你的php.ini中可以手動設置,通過zend.enable_gc這個屬性進行開啟或關閉垃圾回收機制。當開啟了垃圾回收機制后,每當根緩存區存滿時,就會執行上面描述的循環查找算法。根緩存區具有固定的大小,當然你可以通過修改php源碼文件Zend/zend_gc.c中常量GC_ROOT_BUFFER_MAX_ENTRIES來修改根緩存區的大小(注意修改后需要重新編譯php)。當關閉垃圾回收機制后,這個循環查找算法將不會執行,然而可能根會一直存在於根緩沖區中,不管在配置中是否激活了垃圾回收機制。
當然你也可以通過調用gc_enable()和gc_disable()函數來打開和關閉垃圾回收機制,效果和修改配置項相同。即使根緩沖區還沒有滿,也能強制執行周期回收。
php的內存管理機制
現在我們已經知道了zval是怎么回事了。那么現在我們需要知道php的內存管理機制是怎么一回事。
當在執行
- 為變量名分配內存,並存入符號表
- 為變量值分配內存
我們再看代碼:
因為php的核心結構Hashtable,在定義的時候不可能一次性分配足夠多的內存塊,所以初始化的時候只會分配一小塊,等不夠的時候在進行擴容,而Hashtable只擴容不減少,所以當存入100個變量的時候符號表不夠用了就進行一次擴容,當unset()時只是放了為變量值分配的內存,但是為變量名分配的內存還是在符號表中的,符號表並沒有縮小,所以沒收回來的內存是被符號表占去了。
php並不是只要內存不夠就去向OS申請內存,而是先申請一大塊內存,然后將其中一部分分給申請者,這樣再有邏輯需要申請內存的時候,就不需要再向OS申請內存了,避免了重復申請,只有當一大塊內存不夠用的時候再去申請。而當釋放內存時,php並非把內存還給了OS,而是把內存軌道自己維護的空閑內存列表,以便重復利用。
新版本的php(5.3版本之后)是如何處理垃圾內存的?
剛剛上面我們已經講了,針對在php中環形引用導致的垃圾,產生了新的同步算法(GC算法),對於官網上的理論,我進行了理解:
如果一個zval的refcount增加,那么表明該變量的zval還在使用,不屬於垃圾
如果一個zval的refcount減少到0,那么zval可以被釋放掉,可以清除,不是垃圾
如果在經過模擬刪除后一個zval的refcount減1,如果該zval的引用次數為是大於0,那么此zval不能被釋放,可能是一個垃圾
關於垃圾回收的小知識點
unset():unset()只是斷開一個變量到一塊內存區域的連接,同時將該內存區域的引用計數減1,內存是否回收主要還是看refcount是否到0了。
null:將null賦值給一個變量是直接將該變量指向的數據結構置空,同時將其引用計數歸0。
腳本執行結束:該腳本中所有內存都會被釋放,無論是否有環引用。