PHP的GC垃圾收集機制

每一種語言都有自己的自動垃圾回收機制,讓程序員不必過分關(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)珊肃。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子伦乔,更是在濱河造成了極大的恐慌厉亏,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件评矩,死亡現(xiàn)場離奇詭異叶堆,居然都是意外死亡阱飘,警方通過查閱死者的電腦和手機斥杜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來沥匈,“玉大人蔗喂,你說我怎么就攤上這事「咛” “怎么了缰儿?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長散址。 經(jīng)常有香客問我乖阵,道長,這世上最難降的妖魔是什么预麸? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任瞪浸,我火速辦了婚禮,結(jié)果婚禮上吏祸,老公的妹妹穿的比我還像新娘对蒲。我一直安慰自己,他們只是感情好贡翘,可當我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布蹈矮。 她就那樣靜靜地躺著,像睡著了一般鸣驱。 火紅的嫁衣襯著肌膚如雪泛鸟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天踊东,我揣著相機與錄音北滥,去河邊找鬼。 笑死递胧,一個胖子當著我的面吹牛碑韵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播缎脾,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼祝闻,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起联喘,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤华蜒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后豁遭,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體叭喜,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年蓖谢,在試婚紗的時候發(fā)現(xiàn)自己被綠了捂蕴。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡闪幽,死狀恐怖啥辨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情盯腌,我是刑警寧澤溉知,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站腕够,受9級特大地震影響级乍,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜帚湘,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一玫荣、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧客们,春花似錦崇决、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至建邓,卻和暖如春盈厘,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背官边。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工沸手, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人注簿。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓契吉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親诡渴。 傳聞我的和親對象是個殘疾皇子捐晶,可洞房花燭夜當晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容