今天朋友去面試,回來問了一下怎么樣,結果他說一臉懵逼,看來我們平時還是學習的太少了啊。於是比較好奇,果斷問了一下都有哪些問題,朋友說第一個問題就是“描述PHP的垃圾回收機制”,我當時聽了也是一臉茫然,因為平時我們業務邏輯寫的太多,很少去關注這些,但是沒辦法,既然有人問這個問題,看來還是很有必要了解一下的。於是馬上搜了一下,網上資料文章很多,看了幾篇后加上自己的一些理解記錄一下。
首先看了一下官方手冊,只有php5.3版本以后的才有了所謂的新的垃圾回收機制GC,那么以前是怎么干的呢?以前是基於引用計數的方式,這里就需要提一下引用計數的知識,官方手冊里面說php的每個變量都是存在一個叫做zval的容器里面,這個容器不僅包含了這個變量的值和類型,還包含了另外兩個重要的信息,“is_ref”和“refcount”,“is_ref”看名字就應該知道大概和引用相關,它是一個bool值,如果這個值是true那么代表這是一個引用變量,否則是普通變量。“refcount”指的是有多少個變量(符號)指向這個zval容器。
比如一個變量$a="test",如果我們php安裝了xdebug插件並且開啟了插件,就可以用xdebug_debug_zval(“a”)來顯示zval里面的值。這里會輸出a:(refcount=1,is_ref=0)=“test”,可以看到refcount=1,因為這里有一個變量(符號)$a指向了這個zval容器,is_ref=0說明這個存放的是一個普通變量。
如果我們進行一個操作$b=$a呢?按照常規的思路,應該是把$a的值復制一份給$b,然后$b也存放在另一個zval容器中,這個zval容器內容和$a那個一樣。真的是這樣嗎?我們用xdebug_debug_zval(“a”)先輸出$a對應的zval容器值,結果會輸出a:(refcount=2,is_ref=0)="test",這里refcount變成了2 ,說明除了$a還有一個變量(符號)指向這個zval容器,那就是$b了啊,這么一來$a和$b指向的是同一個zval容器,那不是修改$b也會影響到$a了?其實不會的,因為當$b或者$a的值改變的時候,這個zval容器的refcount會減一,然后會復制一份讓改變值的那個變量(符號)指向新的zval容器,這個時候就是我們剛才常規思路想的一樣了,有了兩個zval容器都是(refcount=1,is_ref=0)只是兩個容器的值和類型分別是$a和$b的值和類型。
那如果是引用賦值$c=&$a呢?這時$a和$c同樣也指向同一個zval,即a,c:(refcount=2,is_ref=1)="test",這時候不光refcount加一,is_ref也變成了1也就是true,說明這是引用變量,那么改變$a和$c任何一個都會影響另一個的值。我們如果使用unset($c)的話,$a指向的容器的refcount就會減一變成1。如果我們再unset($a)的話,指向的zval容器的refcount就是0了,這個時候說明已經沒有變量(符號)指向這個容器了,那么php引擎就會從內存中銷毀釋放這個容器。
那如果$a是一個數組呢,它指向的zval容器會是怎樣的?比如$a=array("1","2"),xdebug_debug_zval(“a”)會輸出如下的信息:
a: (refcount=1, is_ref=0)=array (
0 => (refcount=1, is_ref=0)='1',
1 => (refcount=1, is_ref=0)='2'
)
可以看到除了$a本身指向一個zval容器存放外,它的每一個元素也都分別指向一個zval容器,如果我要這樣往$a中添加元素會怎樣?
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning']; //這里直接拿官方示例
這個時候xdebug_debug_zval(“a”)會輸出: key為'meaning'和'life'的值指向同一個zval容器,refcount=2
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( '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,因為$a和$a[1]指向了這個容器,然而$a[1]又是$a的元素,這個元素又引用了$a本身,這就形成了一個閉環。
如果這個時候來一句unset($a)呢?$a指向的容器refcount減一就會變成1,這個時候對於我們程序員來說已經不存在有可操作的變量(符號)指向這個容器了,但是refcount=1那么php引擎就不會銷毀這個容器。
那不是這個容器在內存中不就成了垃圾了嗎?這種情況如果沒有垃圾回收機制GC,那么就只有等到當前請求結束,腳本結束自動清除了。但是有時候我們會用到一些遞歸或者死循環這類的來做一些特殊的業務邏輯,這時候內存如果有上面的情況出現,就會導致內存泄漏,消耗很大的內存空間。
所以才有了5.3版本新的內存回收機制的出現。先說說機制的三個基本規則:
- 如果一個zval容器的refcount增加,說明有新的變量(符號)指向這個容器,那么這個容器當然不會是垃圾,它將被繼續使用。
- 如果一個zval容器的refcount減少到0了,那么說明沒有變量(符號)指向這個容器,它就會被php引擎銷毀。
- 如果一個zval容易的refcount減少了,但是不是0,那么這個容器就有可能是垃圾,就會被垃圾回收機制所管理。
怎么管理這些容器並判斷哪些是垃圾呢?當發現某個容器有可能是垃圾時,這個容器會被放進一個內存緩沖區,當緩沖區滿了時,就會進行垃圾回收算法來找出垃圾並銷毀。這里具體的算法可以看看官方文檔,我就用一個網友的總結來描述:
對於一個包含環形引用的數組,對數組中包含的每個元素的zval進行減1操作,之后如果發現數組自身的zval的refcount變成了0,那么可以判斷這個數組是一個垃圾。
這個道理其實很簡單,假設數組a的refcount等於m, a中有n個元素又指向a,如果m等於n,那么算法的結果是m減n,m-n=0,那么a就是垃圾,如果m>n,那么算法的結果m-n>0,所以a就不是垃圾了。m=n代表什么? 代表a的refcount都來自數組a元素的指向,代表除了a中的元素沒有任何變量(符號)指向根zval容器,代表用戶代碼空間中無法再訪問到這個zval,代表a是泄漏的內存,因此GC將a這個垃圾回收了。
最后在哪里可以設置這個回收機制呢?默認的,PHP的垃圾回收機制是打開的,然后在配置文件 php.ini 里允許你修改它:zend.enable_gc 。除了修改配置zend.enable_gc是否開啟 ,也能通過分別調用gc_enable() 和 gc_disable()函數來打開和關閉垃圾回收機制。調用這些函數,與修改配置項來打開或關閉垃圾回收機制的效果是一樣的。如果想在根緩沖區還沒滿時強制執行周期回收,可以調用gc_collect_cycles()函數,這個函數將返回使用這個算法回收的周期數。
當垃圾回收機制打開時,每當根緩存區存滿時,就會執行查找算法。根緩存區有固定的大小,可存10,000個可能根,當然可以通過修改PHP源碼文件Zend/zend_gc.c中的常量GC_ROOT_BUFFER_MAX_ENTRIES,然后重新編譯PHP,來修改這個10,000值。當垃圾回收機制關閉時,循環查找算法永不執行,然而,可能根將一直存在根緩沖區中,不管在配置中垃圾回收機制是否激活。
當垃圾回收機制關閉時,如果根緩沖區存滿了可能根,更多的可能根顯然不會被記錄。那些沒被記錄的可能根,將不會被這個算法來分析處理。如果他們是循環引用周期的一部分,將永不能被清除進而導致內存泄漏。
即使在垃圾回收機制不可用時,可能根也被記錄的原因是,相對於每次找到可能根后檢查垃圾回收機制是否打開而言,記錄可能根的操作更快。不過垃圾回收和分析機制本身要耗不少時間。
也就是說關閉了回收機制也會往緩沖區丟疑似垃圾的容器,當緩沖區滿了的時候不會執行回收算法,后面更多的疑似垃圾容器不會繼續放進去,就可能導致內存泄漏。當開啟回收機制后,就會從緩沖區中之前放入的容器中開始垃圾回收機制。