PHP垃圾回收機制(一)

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(很全的一篇博客)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發佈了60 篇原創文章 · 獲贊 67 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章