PHP垃圾回收機制,在網上能查到的有早期的,如PHP5.3的垃圾回收機制,也有新的比如PHP7以後的垃圾回收機制。
我覺得有必要先了解一下舊的PHP5.3的垃圾回收機制,原因是簡單一些,主要理解引用計數和寫時複製的概念。
同時也看看早期的存在什麼缺陷,再去了解PHP7的可能會更容易一些,因爲這是在原來的垃圾回收機制基礎上做了修改。
一.PHP5.3的垃圾回收機制
我們知道PHP定義變量的時候是不用區分各種數據類型的,這是因爲PHP替我們做了記錄,每當我們定義一個變量,內存中就會將變量名存入符號表,而將變量的值和類型及其一些信息存入一個叫zval的結構體中(PHP是用C寫成的),舉個下面的例子:
<?php
$a=10;
?>
而這個結構體zval其實長成這個樣子:
truct _zval_struct {
union {
long lval;
double dval;
struct {
char *val;
int len;
} str;
HashTable *ht;
zend_object_value obj;
zend_ast *ast;
} value;
zend_uint refcount__gc;
zend_uchar type;
zend_uchar is_ref__gc;
};
第一項:一個叫做union的聯合體,裏面存儲的是變量的值(value)。
第二項:refcount_gc就是一個整數,用來記錄多少個變量指向這個結構,稍後會詳細介紹。(標紅很重要)
第三項:type記錄的是這個數據的類型,由此去解讀第一項value中的內容。
第四項:is_ref_gc記錄的是這個變量有沒有使用過引用,大白話就是又沒$b=&$a;(標紅很重要)
下面我們來看看使用各種變量的時候內存中發生了什麼:
情況1
<?php
$a=1;
$b=$a;
?>
我們知道$a和$b中存的是同樣的內容,如果再開闢一塊內存空間存儲同樣的內容是很浪費的,PHP早已經想到了這一點,所以
$b=$a的時候並沒有給$b開闢一塊新的內存空間,只是在符號表中增加了$b,讓它指向和$a同樣的結構體。
剛剛定義$a的時候refcount_gc=1,因爲只要$a指向它,同理執行完$b=$a之後zval中的refcount_gc=2 ,這個有什麼用處呢?
接着看:
情況2
<?php
$a =1;
$b = $a;
$b += 5;
?>
從邏輯上可以看到,我們修改了$b的值,而$a的值是不應該改變的。
所以不能用同一個結構體來存儲$a、$b的值了,這時候PHP會在內存中開闢一塊新的空間存儲$b的值,這樣就可以修改$b的值了:
問題來了,內核怎麼知道$a和$b要分家,不應該用同一個zval呢?
答案並不複雜,內核首先查看refcount__gc屬性,如果它大於1則爲這個變化的變量從原zval結構中複製出一份新的專屬與$b的zval來,並改變其值。
所以這個時候$a的zval中refcount__gc=1,$b的zval中refcount__gc=1;
情況3
<?php
$a =1;
$b =&$a;
$b += 5;
?>
從邏輯上我們知道 $b是$a的引用,所以改變$b的時候$a也是要改變的,所以無需開闢新的內存,不用分家。
但是內核是怎麼知道不用分家的呢?
答案就是通過is_ref_gc來判斷。執行$b=&$a的時候,is_ref_gc=1。
簡單的講,它是通過zval的is_ref__gc成員來獲取這些信息的。
這個成員只有兩個值,就像開關的開與關一樣。它的這兩個狀態代表着它是否是一個用戶在PHP語言中定義的引用。
在第一條語句($a = 1;)執行完畢後,$a對應的zval的refcount__gc等於1,is_ref__gc等於0;
當第二條語句執行後($b = &$a;),refcount__gc屬性向往常一樣增長爲2,而且is_ref__gc屬性也同時變爲了1!
最後,在執行第三條語句的時候,內核再次檢查$b的zval以確定是否需要複製出一份新的zval結構來,這次不需要複製.
這一次,儘管它的refcount等於2,但是因爲它的is_ref等於1,所以也不會被複制。
由情況1、情況2、情況3我們可以看到這兩個字段實現了,引用計數和寫時複製的功能。
情況4
<?php
$a = 1;
$b = $a;
$c = &$a;
?>
童鞋們可以思考一下,這種情況內核執行第三句該怎麼處理,還要不要分家呢?
我們可以想一下,如果不分家的話$a、$b、$c指向相同的zval,那麼refcount_gc=3且is_ref_gc=1;
那麼我們這時候修改$b的值,內核會認爲$b、$c都是引用,不會給$b開闢新的內存空間,導致$a($c)的值也改變了。
所以這種情況下,執行第三句代碼肯定要分家,才能不發生上面的矛盾的矛盾。結果就是:
$a與$c共用一個zval,$b自己用一個zval。
同樣,下面的這段代碼同樣會在內核中產生歧義,所以需要強制複製!
<?php
$a = 1;
$b = &$a;
$c = $a;
?>
但是PHP5.3這種垃圾管理方式會存在一個問題,就是存在循環引用導致的內存泄露。
二.PHP5.3循環引用導致的內存泄露
先看代碼:
<?php
$a = ['one'];
$a[] = &$a;
xdebug_debug_zval('a');
?>
注意unset()函數是取消某個變量在符號表中的註冊,如果:
<?php
$a=10;
$b=&$a;
unset($a);
?>
這時候$b依然存在,只是$a在符號表中被取消了。
回到第一段代碼,當我們執行$a[]=&$a,的時候很自然會得到$a指向的zval中:refcount_gc=2,is_ref_gc=1;
此時 $a數組就有了兩個元素,一個索引爲0,值爲one字符串,另一個索引爲1,爲$a自身的引用。
當我們執行unset($a)的時候,$a符號會被註銷掉,同時refcount_gc減1 ,所以refcount_gc=1
$a已經不在符號表了,沒有變量再指向此zval容器,用戶已無法訪問,但是由於數組的refcount變爲1而不是0,導致此部分內存不能被回收從而產生了內存泄漏。
只有在PHP腳本結束的時候才能釋放該部分內存,如果這樣的內存泄露有很多的話,運行速度將產生極大的影響。
所以PHP7的垃圾回收機制對zval做了改動,不僅解決了環形引用導致的內存泄露問題,也做了很多優化。
我將在另一篇博客介紹PHP新的垃圾回收機制。
參考資料:
https://www.cnblogs.com/fengwei/p/3775062.html
https://blog.csdn.net/weixin_41282397/article/details/84969162
https://blog.csdn.net/xuduorui/article/details/76462123(很全的一篇博客)