簡介:PHP 是一門托管型語言敬锐,在 PHP 編程中,程序員不需要手工處理內存資源的分配與釋放(使用 C 編寫 PHP 或 Zend 擴展除外),這就意味著 PHP 本身實現(xiàn)了垃圾回收機制(Garbage Collection)间雀。在?PHP 官方網站可以看到對垃圾回收機制的介紹催什,在開始介紹垃圾回收之前我們先介紹幾個關鍵的概念涵亏。
PHP的引用計數:(refcount洞难,is_ref)
PHP在內核中是通過zval這個結構體來存儲變量的鸵熟,在Zend/zend.h文件中找到了其定義:
PHP5 中定義如下:
struct_zval_struct {
zvalue_valuevalue;
zend_uint refcount;? ? ? ??
zend_uchar type;/* active type */
zend_uchar is_ref;
};
一個例子:
第一步:查看內部結構
$a = 'zd';??
我們可以使用xdebug查看zval的信息
xdebug_debug_zval('a');?
存儲的內部信息是 name:(refcount=1, is_ref=0)='zd'?
說明php通過zavl的refcount變量來存儲引用計數
第二步:增加一個計數
$a = 'zd';? ?$b = $a;
xdebug_debug_zval('a');?
存儲的內部信息是:?name:(refcount=2, is_ref=0),string'zd'?
證明這種情況下并不會開辟兩塊內存空間骇径,并且refcount會加1
第三步驟:引用賦值
當我們使用值引用時豌注,$a = 'zd';? ?$b = &$a;
xdebug_debug_zval('a');?
存儲的內部信息是:name:(refcount=2, is_ref=1)='zd'
所以我們可以的得到的信息是:引用賦值會導致zval通過is_ref來標記是否存在引用的情況夫偶。
第四步:數組型的變量
$test = ['a'=>'1','b'=>'2'];?
xdebug_debug_zval('test');
test: (refcount=1, is_ref=0)=array (?
? ?'a' => (refcount=1, is_ref=0)='1',?
? ? 'b' => (refcount=1,is_ref=0)='2'?
)
由此我們可以明白:數組內部的元素也會生成對應的zavl纤泵,上述的例子存在3個zval,這3個zval都遵循變量的引用和計數原則只恨。
第五步:unset掉變量运怖,使refcount減1
$a = 'zd';
unset($a);
xdebug_debug_zval('a');
a:(refcount=0, is_ref=0)='zd'??
上面提到的refcount=0的情況灵巧,會被自動的銷毀掉搀矫,其實并不屬于垃圾,下面我們來想一想為什么是垃圾刻肄?為什么要有垃圾回收呢瓤球,如果不回收會引入什么問題呢?
PHP5.3 之前的內存泄漏的垃圾回收
產生內存泄漏主要真兇:環(huán)形引用敏弃。 現(xiàn)在來造一個環(huán)形引用的場景:
$a= ['one'];
$a[] = &$a;
xdebug_debug_zval('a');
a: (refcount=2, is_ref=1)=array (
????????0 => (refcount=1, is_ref=0)='one',
????????1 => (refcount=2, is_ref=1)=…
)
“…”表示1指向a自身卦羡,是一個環(huán)形引用
這個時候我們對$a進行unset,那么$a會從符號表中刪除,同時$a指向的zval的refcount減少1麦到,那么問題也就產生了绿饵,$a已經不在符號表中了,用戶無法再訪問此變量瓶颠,但是$a之前指向的zval的refcount由2變?yōu)?而不是0拟赊,因此不能被回收,這樣產生了內存泄露:
為解決這種垃圾粹淋,產生了新的GC
? ? ? ??在較新的PHP手冊中有簡單的介紹新的GC使用的垃圾清理算法吸祟,這個算法名為?Concurrent Cycle Collection in Reference Counted Systems?, 這里不詳細介紹此算法桃移,根據手冊中的內容來先簡單的介紹一下思路:
首先我們有幾個基本的準則:
1:如果一個zval的refcount增加欢搜,那么此zval還在使用,不屬于垃圾
2:如果一個zval的refcount減少到0谴轮,?那么zval可以被釋放掉炒瘟,不屬于垃圾
3:如果一個zval的refcount減少之后大于0,那么此zval還不能被釋放第步,此zval可能成為一個垃圾
只有在準則3下疮装,GC才會把zval收集起來缘琅,然后通過新的算法來判斷此zval是否為垃圾。那么如何判斷這么一個變量是否為真正的垃圾呢廓推?簡單的說刷袍,就是對此zval中的每個元素進行一次refcount減1操作,操作完成之后樊展,如果zval的refcount=0呻纹,那么這個zval就是一個垃圾。這個原理咋看起來很簡單专缠,但是又不是那么容易理解雷酪,起初筆者也無法理解其含義,直到挖掘了源代碼之后才算是了解涝婉。如果你現(xiàn)在不理解沒有關系哥力,后面會詳細介紹,這里先把這算法的幾個步驟描敘一下,首先引用手冊中的一張圖:
A:為了避免每次變量的refcount減少的時候都調用GC的算法進行垃圾判斷墩弯,此算法會先把所有前面準則3情況下的zval節(jié)點放入一個節(jié)點(root)緩沖區(qū)(root buffer)吩跋,并且將這些zval節(jié)點標記成紫色,同時算法必須確保每一個zval節(jié)點在緩沖區(qū)中之出現(xiàn)一次渔工。當緩沖區(qū)被節(jié)點塞滿的時候锌钮,GC才開始開始對緩沖區(qū)中的zval節(jié)點進行垃圾判斷。
B:當緩沖區(qū)滿了之后引矩,算法以深度優(yōu)先對每一個節(jié)點所包含的zval進行減1操作轧粟,為了確保不會對同一個zval的refcount重復執(zhí)行減1操作,一旦zval的refcount減1之后會將zval標記成灰色脓魏。需要強調的是,這個步驟中通惫,起初節(jié)點zval本身不做減1操作茂翔,但是如果節(jié)點zval中包含的zval又指向了節(jié)點zval(環(huán)形引用),那么這個時候需要對節(jié)點zval進行減1操作履腋。
C:算法再次以深度優(yōu)先判斷每一個節(jié)點包含的zval的值珊燎,如果zval的refcount等于0,那么將其標記成白色(代表垃圾)遵湖,如果zval的refcount大于0悔政,那么將對此zval以及其包含的zval進行refcount加1操作,這個是對非垃圾的還原操作延旧,同時將這些zval的顏色變成黑色(zval的默認顏色屬性)
D:遍歷zval節(jié)點谋国,將C中標記成白色的節(jié)點zval釋放掉。
這ABCD四個過程是手冊中對這個算法的介紹迁沫,這還不是那么容易理解其中的原理芦瘾,這個算法到底是個什么意思呢捌蚊?我自己的理解是這樣的:
????????比如還是前面那個變成垃圾的數組$a對應的zval,命名為zval_a,? 如果沒有執(zhí)行unset,?zval_a的refcount為2,分別由$a和$a中的索引1指向這個zval近弟。??
????????用算法對這個數組中的所有元素(索引0和索引1)的zval的refcount進行減1操作缅糟,由于索引1對應的就是zval_a,所以這個時候zval_a的refcount應該變成了1祷愉,這樣zval_a就不是一個垃圾窗宦。
????????如果執(zhí)行了unset操作,zval_a的refcount就是1二鳄,由zval_a中的索引1指向zval_a,用算法對數組中的所有元素(索引0和索引1)的zval的refcount進行減1操作赴涵,這樣zval_a的refcount就會變成0,于是就發(fā)現(xiàn)zval_a是一個垃圾了泥从。 算法就這樣發(fā)現(xiàn)了頑固的垃圾數據句占。
舉了這個例子,讀者大概應該能夠知道其中的端倪:
對于一個包含環(huán)形引用的數組躯嫉,對數組中包含的每個元素的zval進行減1操作纱烘,之后如果發(fā)現(xiàn)數組自身的zval的refcount變成了0,那么可以判斷這個數組是一個垃圾祈餐。
這個道理其實很簡單擂啥,假設數組a的refcount等于m, a中有n個元素又指向a,如果m等于n,那么算法的結果是m減n,m-n=0帆阳,那么a就是垃圾哺壶,如果m>n,那么算法的結果m-n>0,所以a就不是垃圾了
m=n代表什么?? 代表a的refcount都來自數組a自身包含的zval元素,代表a之外沒有任何變量指向它蜒谤,代表用戶代碼空間中無法再訪問到a所對應的zval山宾,代表a是泄漏的內存,因此GC將a這個垃圾回收了鳍徽。
PHP中運用新的GC的算法
在PHP中资锰,GC默認是開啟的,你可以通過ini文件中的zend.enable_gc 項來開啟或則關閉GC阶祭。當GC開啟的時候绷杜,垃圾分析算法將在節(jié)點緩沖區(qū)(roots buffer)滿了之后啟動。緩沖區(qū)默認可以放10,000個節(jié)點濒募,當然你也可以通過修改Zend/zend_gc.c中的GC_ROOT_BUFFER_MAX_ENTRIES?來改變這個數值鞭盟,需要重新編譯鏈接PHP。
當GC關閉的時候瑰剃,垃圾分析算法就不會運行齿诉,但是相關節(jié)點還會被放入節(jié)點緩沖區(qū),這個時候如果緩沖區(qū)節(jié)點已經放滿,那么新的節(jié)點就不會被記錄下來鹃两,這些沒有被記錄下來的節(jié)點就永遠也不會被垃圾分析算法分析遗座。如果這些節(jié)點中有循環(huán)引用,那么有可能產生內存泄漏俊扳。
之所以在GC關閉的時候還要記錄這些節(jié)點途蒋,是因為簡單的記錄這些節(jié)點比在每次產生節(jié)點的時候判斷GC是否開啟更快,另外GC是可以在腳本運行中開啟的馋记,所以記錄下這些節(jié)點号坡,在代碼運行的某個時候如果又開啟了GC,這些節(jié)點就能被分析算法分析梯醒。當然垃圾分析算法是一個比較耗時的操作宽堆。
??? 在PHP代碼中我們可以通過gc_enable()和gc_disable()函數來開啟和關閉GC,也可以通過調用gc_collect_cycles()在節(jié)點緩沖區(qū)未滿的情況下強制執(zhí)行垃圾分析算法茸习。這樣用戶就可以在程序的某些部分關閉或則開啟GC畜隶,也可強制進行垃圾分析算法。
1.unset函數
????unset只是斷開一個變量到一塊內存區(qū)域的連接号胚,同時將該內存區(qū)域的引用計數-1籽慢;內存是否回收主要還是看refount是否到0了,以及gc算法判斷猫胁。
2.= null 操作箱亿;
????a=null是直接將a 指向的數據結構置空,同時將其引用計數歸0弃秆。
3.腳本執(zhí)行結束
????腳本執(zhí)行結束届惋,該腳本中使用的所有內存都會被釋放,不論是否有引用環(huán)菠赚。