PHP 的垃圾回收機制

垃圾回收,簡稱 gc。顧名思義,就是廢物重利用的意思。再說這個之前先接觸一下內存泄露,大概意思就是申請了一塊地兒拉了會兒屎,拉完後不收拾,那麼那塊兒地就算是糟蹋了,地越用越少,最後一地全是屎。說到底一句,用了記得還。一定程度上說,垃圾回收機制就是用來擦屁股的。如果用過 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');

輸出的結果是:

其中,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 變量爲什麼能夠保存字符串 "123" 也能保存數字 123,你知道該怎麼回答了吧?就答出重點 zval 中有該變量的類型,當是字符串 123 的時候,type 就是 string,此時 value 指向 “123”;當是整數 123 的時候,zval 的 type 爲 int,value 爲 123。這就是答題的思想,這很重要!而且,通過 C 語言都是可以實現的!具體真正的 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 依然可能是個垃圾。

什麼樣的情況會導致 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 的關鍵就是能說出引用計數的原理和寫時拷貝,很多細節深處都各種奇奇怪怪的東西,面試官自己都不一定知道。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章