前言php
大多數編程語言都會有自身的垃圾回收機制,php也不例外。常常聽不少人說gc,也就是垃圾回收器,全程爲Garbage Collection。html
在php5.3以前,是不包括垃圾回收機制的,也沒有專門的垃圾回收器,實現垃圾回收就是簡單判斷一下變量的zval的refcount是否爲0,是的話就釋放。node
可是若是這麼簡單的判斷垃圾回收的話,很容易引發程序過程當中內存溢出。若是存在"自身指向自身"的狀況的話,那麼變量將沒法回收早成內存泄露,因此從php5.3開始就出現了專門負責清理垃圾數據防止內存泄露的垃圾回收器。算法
引用計數的基本知識編程
咱們要了解GC,那麼首先要了解引發垃圾回收的基數是什麼。數組
在php中,每一個變量存在一個叫「zval」的變量容器中。一個zval變量容器,除了包含變量的類型和值,還包括另外兩個字節的額外信息。第一個是"is_ref"。第二個是"refcount"。緩存
is_ref是一個布爾類型的值,用來標示這個變量是否屬於引用集合。經過這個字節,php引擎才能把普通變量和引用變量區分開來,因爲php容許用戶經過"&"來使用自定義的引用,因此zval中還有一個內部引用計數機制,來進行優化內存。php7
refcount用來表示這個zval變量容器的變量的個數。全部符號存在一個符號表當中,每一個符號都有做用域。數據結構
通俗的講:編程語言
一、 refcount就是多少個變量是同樣的用了相同的值,那麼refcount就是這個值
二、 is_ref就是當有變量用了&的形式進行賦值,那麼is_ref的值就會增長
<?php$a = "new string"; ?>
在上面的代碼中,變量a是在當前做用於中生成的,而且生成了類型爲String和值爲"new string"的變量容器。這個時候is_ref被默認的設置成了false,由於如今沒有任何自定義的引用生成。refcount被設置成了1。咱們能夠用php來看到這些計數的變化,首先須要用到xdebug,因此php沒有裝上xdebug擴展的須要先裝一下。
<?php
$a = "new string"; xdebug_debug_zval('a');
輸出:a: (refcount=1, is_ref=0)='new string'
?>
增長zval的引用計數
<?php
$a = "new string";
$b = $a;
xdebug_debug_zval( 'a' ); 輸出:a: (refcount=2, is_ref=0)='new string'
?>
這時的引用次數是2,由於同一個變量容器被變量 a 和變量 b關聯.當不必時,php不會去複製已生成的變量容器。變量容器在」refcount「變成0時就被銷燬. 當任何關聯到某個變量容器的變量離開它的做用域(好比:函數執行結束),或者對變量調用了函數 unset()時,」refcount「就會減1。
減小引用計數
<?php
$a = "new string";
$c = $b = $a;
xdebug_debug_zval( 'a' );
unset( $b, $c );
xdebug_debug_zval( 'a' );
輸出: a: (refcount=3, is_ref=0)='new string' a: (refcount=1, is_ref=0)='new string'
?>
執行 unset($a);,包含類型和值的這個變量容器就會從內存中刪除。
複合類型
當變量的類型爲array或object這樣的複合類型時,array和object類型的變量把他們的成員或屬性存在本身的符號表中。
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
xdebug_debug_zval( 'a' );
輸出: a: (refcount=1, is_ref=0)=array ( 'meaning' => (refcount=1, is_ref=0)='life', 'number' => (refcount=1, is_ref=0)=42 )
?>
根據上面的代碼,咱們能夠理解,對於數組來當作一個總體,對於內部的值來看又是一個獨立的總體,各自都有着一套zval的refcount和is_ref。下面這張圖是從官網上扒下來的:
添加一個已經存在的元素到數組中:
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning']; xdebug_debug_zval( 'a' );
輸出: 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' )
?>
以下圖解釋:
從數組中刪除一個元素:
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];unset( $a['meaning'], $a['number'] );
xdebug_debug_zval( 'a' );
輸出: a: (refcount=1, is_ref=0)=array ( 'life' => (refcount=1, is_ref=0)='life' ) ?>
刪除數組中一個元素,就是相似從做用於中刪除一個變量,刪除後數組中這個元素所在容器的refcount的值減小,當refcount爲0時,這個變量容器就從內存中被刪除。
將數組做爲一個元素添加給自身:
<?php
$a = array( '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)=... )
?>
咱們能夠看到數組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的內存管理機制是怎麼一回事。
var_dump(memory_get_usage());
$test = "這是測試啊";
var_dump(memory_get_usage());
unset($name);
var_dump(memory_get_usage());
輸出(php5.6): /var/www/html/node_test/phptest/phptest.php:51: int(361896) /var/www/html/node_test/phptest/phptest.php:53: int(361928) /var/www/html/node_test/phptest/phptest.php:55: int(361896)
過程是:定義變量->內存增長->清除變量->內存恢復
var_dump(memory_get_usage());
$test = "這是測試啊";
var_dump(memory_get_usage());
unset($name);
var_dump(memory_get_usage());
輸出(php7.1): /var/www/html/node_test/phptest/phptest.php:51: int(361896) /var/www/html/node_test/phptest/phptest.php:53: int(361928) /var/www/html/node_test/phptest/phptest.php:55: int(361928)
而我在用php7時發現了這個問題,這就要說道php5和php7的內存管理機制和垃圾回收機制的不一樣了,這裏暫且不表。咱們繼續往下走。
當在執行
$test = "這是測試啊";
內存的分配作了兩件事:
咱們再看代碼:
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());
輸出: /var/www/html/node_test/phptest/phptest.php:57: int(363520) /var/www/html/node_test/phptest/phptest.php:63: int(372384) /var/www/html/node_test/phptest/phptest.php:69: int(369216)
爲何內存沒有所有收回來呢?
由於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。
腳本執行結束:該腳本中全部內存都會被釋放,不管是否有環引用。