轉載請附上本文地址:http://blog.csdn.net/u011957758/article/details/76864400php
是的,平時常常聽到大牛說到的gc,就是垃圾回收器,全稱Garbage Collection。算法
早期版本,準確地說是5.3以前(不包括5.3)的垃圾回收機制,是沒有專門的垃圾回收器的。只是簡單的判斷了一下變量的zval的refcount是否爲0,是的話就釋放不然不釋放直至進程結束。數組
乍一看確實沒毛病啊,然而其中隱藏着變量內存溢出的風險:http://bugs.php.net/bug.php?id=33595 ,沒法回收的內存形成了內存泄漏,因此PHP5.3出現了專門負責清理垃圾數據、防止內存泄漏的GC。bash
下文將由淺入深(憑感受)來記錄下php的垃圾回收機制是怎麼一回事?數據結構
1.php引用計數基本知識點函數
2.php的內存管理機制性能
3.php中垃圾是如何定義的?優化
4.老版本php中如何產生內存泄漏?ui
5.5.3版本之後php是如何處理垃圾內存的?atom
6.涉及到垃圾回收的知識點
首先必需要先講講這個會引發垃圾回收的關鍵基數是怎麼回事?
關於php的zval結構體,以及refcount與is_ref的知識點,在菜鳥學php擴展 之 詳解php擴展的變量(四) 已描述很是清楚。
不許確但卻通俗的說:
refcount:多少個變量是同樣的用了相同的值,這個數值就是多少。
is_ref:bool類型,當refcount大於2的時候,其中一個變量用了地址&的形式進行賦值,好了,它就變成1了。
主要講講如何用php來直觀的看到這些計數的變化,走一波。
首先須要在php上裝上xdebug的擴展。
1.第一步:查看內部結構
<?php $name = "咖啡色的羊駝"; xdebug_debug_zval('name');
會獲得:
name:(refcount=1, is_ref=0),string '咖啡色的羊駝' (length=18)
2.第二步:增長一個計數
<?php $name = "咖啡色的羊駝"; $temp_name = $name; xdebug_debug_zval('name');
會獲得:
name:(refcount=2, is_ref=0),string '咖啡色的羊駝' (length=18)
看到了吧,refcount+1了。
3.第三步:引用賦值
<?php $name = "咖啡色的羊駝"; $temp_name = &$name; xdebug_debug_zval('name');
會獲得:
name:(refcount=2, is_ref=1),string '咖啡色的羊駝' (length=18)
是的引用賦值會致使zval經過is_ref來標記是否存在引用的狀況。
4.第四步:數組型的變量
<?php $name = ['a'=>'咖啡色', 'b'=>'的羊駝']; xdebug_debug_zval('name');
會獲得:
name:
(refcount=1, is_ref=0), array (size=2) 'a' => (refcount=1, is_ref=0),string '咖啡色' (length=9) 'b' => (refcount=1, is_ref=0),string '的羊駝' (length=9)
還挺好理解的,對於數組來看是一個總體,對於內部kv來看又是分別獨立的總體,各自都維護着一套zval的refount和is_ref。
5.第五步:銷燬變量
<?php $name = "咖啡色的羊駝"; $temp_name = $name; xdebug_debug_zval('name'); unset($temp_name); xdebug_debug_zval('name');
會獲得:
name:(refcount=2, is_ref=0),string '咖啡色的羊駝' (length=18) name:(refcount=1, is_ref=0),string '咖啡色的羊駝' (length=18)
refcount計數減1,說明unset並不是必定會釋放內存,當有兩個變量指向的時候,並不是會釋放變量佔用的內存,只是refcount減1.
知道了zval是怎麼一回事,接下來看看如何經過php直觀看到內存管理的機制是怎麼樣的。
先來一段代碼:
<?php //獲取內存方法,加上true返回實際內存,不加則返回表現內存 var_dump(memory_get_usage()); $name = "咖啡色的羊駝"; var_dump(memory_get_usage()); unset($name); var_dump(memory_get_usage());
會獲得:
int 1593248 int 1593384 int 1593248
大體過程:定義變量->內存增長->清除變量->內存恢復
當執行:
$name = "咖啡色的羊駝";
時候,內存的分配作了兩件事情:1.爲變量名分配內存,存入符號表 2.爲變量值分配內存
再來看代碼:
<?php var_dump(memory_get_usage()); for($i=0;$i<100;$i++) { $a = "test".$i; $$a = "hello"; } var_dump(memory_get_usage()); for($i=0;$i<100;$i++) { $a = "test".$i; unset($$a); } var_dump(memory_get_usage());
會獲得:
int 1596864 int 1612080 int 1597680
簡直爆炸,怎麼和以前看的不同?內存沒有所有回收回來。
對於php的核心結構Hashtable來講,因爲未知性,定義的時候不可能一次性分配足夠多的內存塊。因此初始化的時候只會分配一小塊,等不夠的時候在進行擴容,而Hashtable只擴容不減小,因此就出現了上述的狀況:當存入100個變量的時候,符號表不夠用了就進行一次擴容,當unset的時候只釋放了」爲變量值分配內存」,而「爲變量名分配內存」是在符號表的,符號表並無縮小,因此沒收回來的內存是被符號表佔去了。
php和c語言同樣,也是須要進行申請內存的,只不過這些操做做者都封裝到底層了,php使用者無感知而已。
php的內存申請小設計
php並不是簡單的向os申請內存,而是會申請一大塊內存,把其中一部分分給申請者,這樣當再有邏輯來申請內存的時候,就不須要向os申請了,避免了頻繁調用。當內存不夠的時候纔會再次申請
php的內存釋放小設計
當釋放內存的時候,php並不是會把內存還給os,而是把內存軌道本身維護的空閒內存列表,以便重複利用,
準確地說,判斷是否爲垃圾,主要看有沒有變量名指向變量容器zval,若是沒有則認爲是垃圾,須要釋放。
打個比方:
<?php $name = "咖啡色的羊駝"; // todo other things
當定義name的時候,處理完字符串準備作其他事情的時候,對於我們來說name就是能夠回收的垃圾了,然而對於引擎來講,$name仍是實打實存在的refcount也仍是1,因此就不是垃圾,不能回收。當調用unset的時候,也並不必定引擎會認爲它是一個垃圾而進行回收,主要仍是看refcount是否是真的變爲0了。
產生內存泄漏主要真兇:環形引用。
如今來造一個環形引用的場景:
<?php $a = ['one']; $a[] = &$a; xdebug_debug_zval('a');
獲得:
a:
(refcount=2, is_ref=1), array (size=2) 0 => (refcount=1, is_ref=0),string 'one' (length=3) 1 => (refcount=2, is_ref=1), &array<
這樣 $a數組就有了兩個元素,一個索引爲0,值爲one字符串,另外一個索引爲1,爲$a自身的引用。
此時刪掉$a:
<?php $a = ['one']; $a[] = &$a; unset($a);
若是在小於php5.3的版本就會出現一個問題:$a已經不在符號表了,沒有變量再指向此zval容器,用戶已沒法訪問,可是因爲數組的refcount變爲1而不是0,致使此部份內存不能被回收從而產生了內存泄漏。
爲解決環形引用致使的垃圾,產生了新的GC算法,遵照如下幾個基本準則:
1.若是一個zval的refcount增長,那麼此zval還在使用,不屬於垃圾
2.若是一個zval的refcount減小到0, 那麼zval能夠被釋放掉,不屬於垃圾
3.若是一個zval的refcount減小以後大於0,那麼此zval還不能被釋放,此zval可能成爲一個垃圾
are you ok?
來個白話文版:就是對此zval中的每一個元素進行一次refcount減1操做,操做完成以後,若是zval的refcount=0,那麼這個zval就是一個垃圾
引用php官方手冊的配圖:
A:爲了不每次變量的refcount減小的時候都調用GC的算法進行垃圾判斷,此算法會先把全部前面準則3狀況下的zval節點放入一個節點(root)緩衝區(root buffer),而且將這些zval節點標記成紫色,同時算法必須確保每個zval節點在緩衝區中之出現一次。當緩衝區被節點塞滿的時候,GC纔開始開始對緩衝區中的zval節點進行垃圾判斷。
B:當緩衝區滿了以後,算法以深度優先對每個節點所包含的zval進行減1操做,爲了確保不會對同一個zval的refcount重複執行減1操做,一旦zval的refcount減1以後會將zval標記成灰色。須要強調的是,這個步驟中,起初節點zval自己不作減1操做,可是若是節點zval中包含的zval又指向了節點zval(環形引用),那麼這個時候須要對節點zval進行減1操做。
C:算法再次以深度優先判斷每個節點包含的zval的值,若是zval的refcount等於0,那麼將其標記成白色(表明垃圾),若是zval的refcount大於0,那麼將對此zval以及其包含的zval進行refcount加1操做,這個是對非垃圾的還原操做,同時將這些zval的顏色變成黑色(zval的默認顏色屬性)
D:遍歷zval節點,將C中標記成白色的節點zval釋放掉。
are you ok?
來個白話文版的:
例如:
<?php $a = ['one']; --- zval_a(將$a對應的zval,命名爲zval_a) $a[] = &$a; --- step1 unset($a); --- step2
爲進行unset以前(step1),進行算法計算,對這個數組中的全部元素(索引0和索引1)的zval的refcount進行減1操做,因爲索引1對應的就是zval_a,因此這個時候zval_a的refcount應該變成了1,這樣說明zval_a不是一個垃圾不進行回收。
當執行unset的時候(step2),進行算法計算,因爲環形引用,上文得出會有垃圾的結構體,zval_a的refcount是1(zval_a中的索引1指向zval_a),用算法對數組中的全部元素(索引0和索引1)的zval的refcount進行減1操做,這樣zval_a的refcount就會變成0,因而就認爲zval_a是一個須要回收的垃圾。
算法總的套路:對於一個包含環形引用的數組,對數組中包含的每一個元素的zval進行減1操做,以後若是發現數組自身的zval的refcount變成了0,那麼能夠判斷這個數組是一個垃圾。
可能會發現,每次都進行這樣的操做好像會影響性能,是的,php作事情套路都是走批量的原則。
申請內存也是申請一大塊,僅使用當前的一小部分剩下的等下回再用,避免屢次申請。
這個gc算法也是這樣,會有一個緩衝區的概念,等緩衝區滿了纔會一次性去給清掉。
開關配置
php.ini中設置 zend.enable_gc 項來開啓或則關閉GC。
緩衝區配置
緩衝區默承認以放10,000個節點,當緩衝區滿了纔會清理。能夠經過修改Zend/zend_gc.c中的GC_ROOT_BUFFER_MAX_ENTRIES 來改變這個數值,須要從新編譯連接PHP
關鍵函數
gc_enable() : 開啓GC
gc_disable() : 關閉GC
gc_collect_cycles() : 在節點緩衝區未滿的狀況下強制執行垃圾分析算法
1.unset函數
unset只是斷開一個變量到一塊內存區域的鏈接,同時將該內存區域的引用計數-1;內存是否回收主要仍是看refount是否到0了,以及gc算法判斷。
2.= null 操做;
a=null是直接將a 指向的數據結構置空,同時將其引用計數歸0。
3.腳本執行結束
腳本執行結束,該腳本中使用的全部內存都會被釋放,不管是否有引用環。