1. 程式人生 > 程式設計 >分析PHP的垃圾回收機制

分析PHP的垃圾回收機制

如果用過C語言,那麼申請記憶體的方式是malloc或者是calloc,然後你用完這個記憶體後,一定不要忘了用free函式去釋放掉,這就是傳說中手動垃圾回收,一般都是掃地神僧用這種方式。很多高層次語言中,你這輩子都是接觸不到記憶體管理的,比如世界上最好的語言php,這種語言替你管理了記憶體,你就安安心心寫爛程式碼即可。寫php的,你說你關心內程式設計客棧存,我是不怎麼相信的,一定是你在裝逼。當然了,如果你用的swoole或者wm或者自己發明的常駐記憶體級php應用,那你將不得不關注記憶體洩露問題,也就說一定要記得釋放無用變數。那麼,在用的最普遍地最傳統的web開發中,php的自動垃圾回收機制是怎樣的呢?這個問題我們先這麼想,就是都知道php是C語言實現的,現在把C語言給你放在這裡了,然後你想想如何用C語言實現對一個變數的統計以及釋放。你不要想如何實現php,你就想C語言如何實現一個變數,從宣告開始到最後沒人用了,就把這個變數所佔的記憶體給釋放掉。你從這個角度出發,就會舒服一些,這不再是一個技術難題,而是一個傻逼產品經理提的一個傻逼需求。好了,步入正題,PHP進行記憶體管理的核心演算法一共兩項:一是引用計數,二是寫時拷貝,請理(bei)解(song)。當你宣告一個PHP變數的時候,C語言就在底層給你搞了一個叫做zval的struct(結構體);如果你還給這個變數賦值了,比如“hello world”,那麼C語言就在底層再給你搞一個叫做zend_value的union(聯合體),總體看來就是這樣的:

好了,進入程式碼實戰階段,注意兩點:

$a = 'hello'. mt_rand( 1,1000 );
echo xdebug_debug_zval( 'a');
$b = $a;
echo xdebug_debug_zval( 'a');
$c = $a;
echo xdebug_debug_zval( 'a');
unset( $c );
echo xdebug_debug_zval( 'a');

輸出的結果是:

分析PHP的垃圾回收機制

其中,zval struct結構體用於儲存$a,zend_value union聯合體用於儲存資料內容也就是'hello916'。由於後面又聲明瞭b和c,所以C不得不又在底層給你搞出兩個zval struct結構體來。

其中,zval和zend value的結構大概如下:(注意!!!這並不是完整正確的PHP zval和zend_value在C語言中struct和union實現,僅僅是挑出最重點的部分寫出來,強調一下:你沒有必要一個字不差背誦過zval和zend_value,你只需要知道原理)

zval {

string "a" //變數的名字是a

value zend_value //變數的值

type string //變數是字串型別

}

zend_value {

string "hello916" //值的內容

refcount 1 //引用計數

}

看到上面兩個,如果面試官問你php變數為什麼TzMODnmlB能夠儲存字串"123"也能儲存數字123,你知道該怎麼回答了吧?就答出重點zval中有該變數的型別,當是字串123的時候,type就是string,此時value指向“123”;當是整數123的時候,zval的type為int,value為123。這就是答題的思想,這很重要!而且,通過C語言http://www.cppcns.com

都是可以實現的!具體真正的val和zend_value的模樣,有興趣的同學可以去網上搜搜,如果你沒有C語言的底子,可能比較吃力!前者是一個struct結構體,後者是一個union聯合體!

這個refcount就是傳說中的引用計數了,初始化的時候a後面的引用次數為1(注意,正確說法應該是a後面的賦值的陣列zend_value引用計數為1,而不是a這個變數zval本身)。然後我們將$b = $a,其實相當於又一個變數指向了這個zend_value,所以refcount變為2,最後將$c = $a,同理,zend_value的refcount再次加1變成了3。然後,我們用unset( $c ),這會兒,C語言要做的就是把$c的zval給KO free掉,但是並不是free zend_value,這會兒zend_value的refcount就自然而然減1變成2了。

那麼寫時拷貝是什麼意思呢?看下面程式碼:

<?php
// 先不要問為什麼非要加mt_rand,不然,絕筆說不過來了,到處都是坑
$a = 'hello'. mt_rand( 1,1000 );
$b = $a;
$a = 123;
echo $b. PHP_EOL;

// 執行結果,不用我說吧,腳趾頭都知道是'hello'.mt_rand( 1,1000 )的結果,絕對不可能是123。

其實,當你把$a賦值給$b的時候,$a的值並沒有真的複製了一份,這樣是對記憶體的極度不尊重,也是對時間複雜度的極度不尊重,計算機僅僅是將$b指向了$a的值而已,這就叫多快好省。那麼,什麼時候真正的發生複製呢?就是當我們修改$a的值為123的時候,這個時候就不得已進行復制,避免$b的值和$a的一樣。

<?php
$a = 'hello'. mt_rand( 1,1000 );
$b = $a;
echo xdebug_debug_zval( 'a');
$a = 'world'. mt_rand( 2,2000 );
echo xdebug_debug_zval( 'a');

// 執行結果為1,其中的原理你自己應該能理順了昂

叨逼叨了這麼長,通過簡單的案例解釋清楚了兩個要點:引用計數和寫時拷貝,那麼垃圾回收也該來了。當一個zval在被unset的時候、或者從一個函式中執行完畢出來(就是區域性變數)的時候等等很多地方,都會產生zval與zend_value發生斷開的行為,這個時候zend引擎需要檢測的就是zend_value的refcount是否為0,如果為0,則直接KO free空出內容來。如果zend_value的recount不為0(廢話一定是大於0),這個value不能被釋放,但是也不代表這個zend_value是清白的,因為此zend_value依然可能是個垃http://www.cppcns.com圾。

什麼樣的情況會導致zend_value的refcount不為0,但是這個zend_value卻是個垃圾呢?PHP7種兩種情況:

<?php
$arr = [ 1 ];
$arr[] = &$arr;
unset( $arr );

這種情況下,zend_value不會能釋放,但也不能放過它,不然一定會產生記憶體洩漏,所以這會兒zend_value會被扔到一個叫做垃圾回收堆中,然後zend引擎會依次對垃圾回收堆中的這些zend_value進行二次檢測,檢測是不是由於上述兩種情況造成的refcount為1但是自身卻確實沒有人再用了,如果一旦確定是上述兩種情況造成的,那麼就會將zend_value徹底抹掉釋放記憶體。

那麼垃圾回收發生在什麼時候?有些同學可能有疑問,就是php不是執行一次就銷燬了嗎,我要著gc有何用?並不是啦,首先當一次fpm執行完畢後,最後一定還有gc的,這個銷燬就是gc;其次是,記憶體都是即用即釋放的,而不是攢著非得到最後,你想想一個典型的場景,你的控制器裡的某個方法裡用了一個函式,函式需要一個巨大的陣列引數,然後函式還需要修改這個巨大的陣列引數,你們應該是函式的執行範圍裡面修改這個陣列,所以此時會發生寫時拷貝了,當函式執行完畢後,就得趕緊釋放掉這塊兒記憶體以供給其他程序使用,而不是非得等到本地fpm request徹底完成後才銷燬。

說到最後,說些自己的話:大多數情況下,面試官問你問題主要是想一是要你個思維思路,二是看你學習程度。就像gc這個問題,其實很多指令碼語言的垃圾回收機制基本上都是靠引用計數和寫時拷貝這兩種演算法結合完成的,所以如果你設計一門指令碼語言,gc機制就按照這兩種演算法進行設計即可。其次是大多數phper不會看這些東西的,面試官問你這個問題不是要你死記硬背那麼多細節,你背不過的,他還是想探測你平時有沒有更積極程式設計客棧地往深層發展的心態。

注重體現重點,很多細節實在沒法寫,比如我舉個例子$a=[],xdebug_debug_zval( $a )的refcount值你猜是多少? 7.1.17下竟然是2,你是不是以為是1,然而並不是。不過你不用糾結這些細節,gc的關鍵就是能說出引用計數的原理和寫時拷貝,很多細節深處都各種奇奇怪怪的東西,面試官自己都不一定知道。

以上就是淺談PHP的垃圾回收機制的詳細內容,更多關於PHP的垃圾回收機制的資料請關注我們其它相關文章!