每一種語言都有自己的自動垃圾回收機制,讓程序員不必過分關(guān)心程序內(nèi)存分配,但是在OOP中势誊,有些對象需要顯式的銷毀粟耻;防止程序執(zhí)行內(nèi)存溢出挤忙。
PHP 垃圾回收機制(Garbage Collector 簡稱GC)
? ? ? ? 在PHP中册烈,沒有任何變量指向這個對象時,這個對象就成為垃圾扭倾。PHP會將其在內(nèi)存中銷毀;這是PHP 的GC垃圾處理機制驾中,防止內(nèi)存溢出模聋。
????????當一個 PHP線程結(jié)束時此改,當前占用的所有內(nèi)存空間都會被銷毀共啃,當前程序中所有對象同時被銷毀究珊。GC進程一般都跟著每起一個SESSION而開始運行的剿涮。
????????GC目的是為了在session文件過期以后自動銷毀刪除這些文件.
__destruct /unset
????????__destruct() 析構(gòu)函數(shù),是在垃圾對象被回收時執(zhí)行瞬浓。
????????unset 銷毀的是指向?qū)ο蟮淖兞浚皇沁@個對象蓬坡。
Session 與 GC
????????由于PHP的工作機制猿棉,它并沒有一個daemon線程來定期的掃描Session 信息并判斷其是否失效,當一個有效的請求發(fā)生時屑咳,PHP 會根據(jù)全局變量 session.gc_probability 和session.gc_divisor的值萨赁,來決定是否啟用一個GC, 在默認情況下, session.gc_probability=1, session.gc_divisor =100 也就是說有1%的可能性啟動GC(也就是說100個請求中只有一個gc會伴隨100個中的某個請求而啟動).
????????GC 的工作就是掃描所有的Session信息兆龙,用當前時間減去session最后修改的時間杖爽,同session.gc_maxlifetime參數(shù)進行比較,如果生存時間超過gc_maxlifetime(默認24分鐘) ,就將該session刪除紫皇。
????????但是掂林,如果你Web服務(wù)器有多個站點锣杂,多個站點時,GC處理session可能會出現(xiàn)意想不到的結(jié)果蝶押,原因就是:GC在工作時企锌,并不會區(qū)分不同站點的session.
那么這個時候怎么解決呢?
????????1. 修改session.save_path,或使用session_save_path() 讓每個站點的session保存到一個專用目錄,
????????2. 提供GC的啟動率捌肴,自然孽查,GC的啟動率提高答朋,系統(tǒng)的性能也會相應(yīng)減低洪规,不推薦念赶。
????????3. 在代碼中判斷當前session的生存時間牺勾,利用session_destroy()刪除.
什么算垃圾
????首先我們需要定義一下“垃圾”的概念,新的GC負責清理的垃圾是指變量的容器zval還存在,但是又沒有任何變量名指向此zval民泵。因此GC判斷是否為垃圾的一個重要標準是有沒有變量名指向變量容器zval嬉橙。
??? 假設(shè)我們有一段PHP代碼枫振,使用了一個臨時變量$tmp存儲了一個字符串额衙,在處理完字符串之后硼啤,就不需要這個$tmp變量了咧织,$tmp變量對于我們來說可以算是一個“垃圾”了,但是對于GC來說为迈,$tmp其實并不是一個垃圾剂陡,$tmp變量對我們沒有意義晓锻,但是這個變量實際還存在,$tmp符號依然指向它所對應(yīng)的zval色鸳,GC會認為PHP代碼中可能還會使用到此變量,所以不會將其定義為垃圾缀匕。
??? 那么如果我們在PHP代碼中使用完$tmp后乡小,調(diào)用unset刪除這個變量苗分,那么$tmp是不是就成為一個垃圾了呢纬向。很可惜,GC仍然不認為$tmp是一個垃圾糕篇,因為$tmp在unset之后饰迹,refcount減少1變成了0(這里假設(shè)沒有別的變量和$tmp指向相同的zval),這個時候GC會直接將$tmp對應(yīng)的zval的內(nèi)存空間釋放赂摆,$tmp和其對應(yīng)的zval就根本不存在了挟憔。此時的$tmp也不是新的GC所要對付的那種“垃圾”。那么新的GC究竟要對付什么樣的垃圾呢烟号,下面我們將生產(chǎn)一個這樣的垃圾绊谭。??
頑固垃圾的產(chǎn)生過程
??? 如果讀者已經(jīng)閱讀了變量內(nèi)部存儲相關(guān)的內(nèi)容,想必對refcount和isref這些變量內(nèi)部的信息有了一定的了解汪拥。這里我們將結(jié)合手冊中的一個例子來介紹垃圾的產(chǎn)生過程:
<?php
????????$a = "new string";
?>
在這么簡單的一個代碼中达传,$a變量內(nèi)部存儲信息為
????????a: (refcount=1, is_ref=0)='new string'
當把$a賦值給另外一個變量的時候,$a對應(yīng)的zval的refcount會加1
<?php
????$a = "new string";
????$b = $a;
?>
此時$a和$b變量對應(yīng)的內(nèi)部存儲信息為
????a,b: (refcount=2, is_ref=0)='new string'
當我們用unset刪除$b變量的時候迫筑,$b對應(yīng)的zval的refcount會減少1
<?php
????????$a = "new string"; //a: (refcount=1, is_ref=0)='new string'
????????$b = $a;???????????????? //a,b: (refcount=2, is_ref=0)='new string'
????????unset($b);???????????? ?//a: (refcount=1, is_ref=0)='new string'
?>
對于普通的變量來說宪赶,這一切似乎很正常,但是在復合類型變量(數(shù)組和對象)中脯燃,會發(fā)生比較有意思的事情:
<?php
????????$a = array('meaning' => 'life', 'number' => 42);
?>
a的內(nèi)部存儲信息為:
a: (refcount=1, is_ref=0)=array (
????'meaning' => (refcount=1, is_ref=0)='life',
????'number' => (refcount=1, is_ref=0)=42
)
數(shù)組變量本身($a)在引擎內(nèi)部實際上是一個哈希表搂妻,這張表中有兩個zval項 meaning和number,
所以實際上那一行代碼中一共生成了3個zval,這3個zval都遵循變量的引用和計數(shù)原則辕棚,用圖來表示:
?下面在$a中添加一個元素欲主,并將現(xiàn)有的一個元素的值賦給新的元素:
<?php
????????$a = array('meaning' => 'life', 'number' => 42);
????????$a['life'] = $a['meaning'];
?>
那么$a的內(nèi)部存儲為:
a: (refcount=1, is_ref=0)=array (
????????'meaning' => (refcount=2, is_ref=0)='life',
????????'number' => (refcount=1, is_ref=0)=42,
????????'life' => (refcount=2, is_ref=0)='life'
)
其中的meaning元素和life元素之指向同一個zval的:
現(xiàn)在,如果我們試一下逝嚎,將數(shù)組的引用賦值給數(shù)組中的一個元素岛蚤,有意思的事情就發(fā)生了:
<?php?
????????$a = array('one');
????????$a[] = &$a;
?>
這樣$a數(shù)組就有兩個元素,一個索引為0懈糯,值為字符one,另外一個索引為1涤妒,為$a自身的引用,內(nèi)部存儲如下:
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
<?php
????????$a = array('one');
????????$a[] = &$a;
????????unset($a);
?>
那么問題也就產(chǎn)生了,$a已經(jīng)不在符號表中了屿储,用戶無法再訪問此變量贿讹,但是$a之前指向的zval的refcount變?yōu)?而不是0,因此不能被回收够掠,這樣產(chǎn)生了內(nèi)存泄露:
這樣民褂,這么一個zval就成為了一個真是意義的垃圾了,新的GC要做的工作就是清理這種垃圾疯潭。
為解決這種垃圾赊堪,產(chǎn)生了新的GC
???在PHP5.3版本中,使用了專門GC機制清理垃圾竖哩,在之前的版本中是沒有專門的GC哭廉,那么垃圾產(chǎn)生的時候,沒有辦法清理相叁,內(nèi)存就白白浪費掉了遵绰。在PHP5.3源代碼中多了以下文件:{PHPSRC}/Zend/zend_gc.h {PHPSRC}/Zend/zend_gc.c, 這里就是新的GC的實現(xiàn)辽幌,我們先簡單的介紹一下算法思路,然后再從源碼的角度詳細介紹引擎中如何實現(xiàn)這個算法的椿访。
新的GC算法
????在較新的PHP手冊中有簡單的介紹新的GC使用的垃圾清理算法乌企,這個算法名為?Concurrent Cycle Collection in Reference Counted Systems?, 這里不詳細介紹此算法成玫,根據(jù)手冊中的內(nèi)容來先簡單的介紹一下思路:
首先我們有幾個基本的準則:
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)在不理解沒有關(guān)系,后面會詳細介紹霸奕,這里先把這算法的幾個步驟描敘一下,首先引用手冊中的一張圖:
A:為了避免每次變量的refcount減少的時候都調(diào)用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標記成灰色吃沪。需要強調(diào)的是,這個步驟中什猖,起初節(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四個過程是手冊中對這個算法的介紹噪服,這還不是那么容易理解其中的原理毡泻,這個算法到底是個什么意思呢?我自己的理解是這樣的:
????????比如還是前面那個變成垃圾的數(shù)組$a對應(yīng)的zval,命名為zval_a,? 如果沒有執(zhí)行unset粘优,?zval_a的refcount為2,分別由$a和$a中的索引1指向這個zval仇味。??
????????用算法對這個數(shù)組中的所有元素(索引0和索引1)的zval的refcount進行減1操作,由于索引1對應(yīng)的就是zval_a雹顺,所以這個時候zval_a的refcount應(yīng)該變成了1丹墨,這樣zval_a就不是一個垃圾。
????????如果執(zhí)行了unset操作嬉愧,zval_a的refcount就是1贩挣,由zval_a中的索引1指向zval_a,用算法對數(shù)組中的所有元素(索引0和索引1)的zval的refcount進行減1操作,這樣zval_a的refcount就會變成0没酣,于是就發(fā)現(xiàn)zval_a是一個垃圾了揽惹。 算法就這樣發(fā)現(xiàn)了頑固的垃圾數(shù)據(jù)。
舉了這個例子四康,讀者大概應(yīng)該能夠知道其中的端倪:
對于一個包含環(huán)形引用的數(shù)組搪搏,對數(shù)組中包含的每個元素的zval進行減1操作,之后如果發(fā)現(xiàn)數(shù)組自身的zval的refcount變成了0闪金,那么可以判斷這個數(shù)組是一個垃圾疯溺。
這個道理其實很簡單,假設(shè)數(shù)組a的refcount等于m, a中有n個元素又指向a,如果m等于n,那么算法的結(jié)果是m減n哎垦,m-n=0囱嫩,那么a就是垃圾,如果m>n,那么算法的結(jié)果m-n>0,所以a就不是垃圾了
m=n代表什么漏设?? 代表a的refcount都來自數(shù)組a自身包含的zval元素,代表a之外沒有任何變量指向它墨闲,代表用戶代碼空間中無法再訪問到a所對應(yīng)的zval,代表a是泄漏的內(nèi)存郑口,因此GC將a這個垃圾回收了鸳碧。
PHP中運用新的GC的算法
在PHP中盾鳞,GC默認是開啟的,你可以通過ini文件中的zend.enable_gc 項來開啟或則關(guān)閉GC瞻离。當GC開啟的時候腾仅,垃圾分析算法將在節(jié)點緩沖區(qū)(roots buffer)滿了之后啟動。緩沖區(qū)默認可以放10,000個節(jié)點套利,當然你也可以通過修改Zend/zend_gc.c中的GC_ROOT_BUFFER_MAX_ENTRIES?來改變這個數(shù)值推励,需要重新編譯鏈接PHP。
當GC關(guān)閉的時候肉迫,垃圾分析算法就不會運行验辞,但是相關(guān)節(jié)點還會被放入節(jié)點緩沖區(qū),這個時候如果緩沖區(qū)節(jié)點已經(jīng)放滿喊衫,那么新的節(jié)點就不會被記錄下來跌造,這些沒有被記錄下來的節(jié)點就永遠也不會被垃圾分析算法分析。如果這些節(jié)點中有循環(huán)引用格侯,那么有可能產(chǎn)生內(nèi)存泄漏鼻听。
之所以在GC關(guān)閉的時候還要記錄這些節(jié)點,是因為簡單的記錄這些節(jié)點比在每次產(chǎn)生節(jié)點的時候判斷GC是否開啟更快联四,另外GC是可以在腳本運行中開啟的撑碴,所以記錄下這些節(jié)點,在代碼運行的某個時候如果又開啟了GC朝墩,這些節(jié)點就能被分析算法分析醉拓。當然垃圾分析算法是一個比較耗時的操作。
??? 在PHP代碼中我們可以通過gc_enable()和gc_disable()函數(shù)來開啟和關(guān)閉GC收苏,也可以通過調(diào)用gc_collect_cycles()在節(jié)點緩沖區(qū)未滿的情況下強制執(zhí)行垃圾分析算法亿卤。這樣用戶就可以在程序的某些部分關(guān)閉或則開啟GC,也可強制進行垃圾分析算法鹿霸。
涉及到垃圾回收的知識點
1.unset函數(shù)
????unset只是斷開一個變量到一塊內(nèi)存區(qū)域的連接排吴,同時將該內(nèi)存區(qū)域的引用計數(shù)-1;內(nèi)存是否回收主要還是看refount是否到0了懦鼠,以及gc算法判斷钻哩。
2.= null 操作;
????a=null是直接將a 指向的數(shù)據(jù)結(jié)構(gòu)置空肛冶,同時將其引用計數(shù)歸0街氢。
3.腳本執(zhí)行結(jié)束
????腳本執(zhí)行結(jié)束,該腳本中使用的所有內(nèi)存都會被釋放睦袖,不論是否有引用環(huán)珊肃。