深入淺出PHP垃圾回收機制

  • php引用計數基本知識點

首先必須要先講講這個會引起垃圾回收的關鍵基數是怎麼回事?

關於php的zval結構體,以及refcount與is_ref的知識點

refcount:多少個變量是一樣的用了相同的值,這個數值就是多少。
is_ref:bool類型,當refcount大於2的時候,其中一個變量用了地址&的形式進行賦值,好了,它就變成1了。

主要講講如何用php來直觀的看到這些計數的變化,首先需要在php上裝上xdebug的擴展。

1.查看內部結構

<?php
    $name = "咖啡色的羊駝";
    xdebug_debug_zval('name');
    ?>

會得到:

name:(refcount=1, is_ref=0),string '咖啡色的羊駝' (length=18)

2.增加一個計數

<?php
        $name = "咖啡色的羊駝";
        $temp_name = $name;
        xdebug_debug_zval('name');

會得到:

name:(refcount=2, is_ref=0),string '咖啡色的羊駝' (length=18)

此時refcount+1。

3.引用賦值

<?php
    $name = "咖啡色的羊駝";
    $temp_name = &$name;
    xdebug_debug_zval('name');

會得到:

name:(refcount=2, is_ref=1),string '咖啡色的羊駝' (length=18)

引用賦值會導致zval通過is_ref來標記是否存在引用的情況。

4.數組型的變量

<?php
    $name = ['a'=>'咖啡色', 'b'=>'的羊駝'];
    xdebug_debug_zval('name');

會得到:

name:
(refcount=1, is_ref=0),
array (size=2)
  'a' => (refcount=1, is_ref=0),string '咖啡色' (length=9)
  'b' => (refcount=1, is_ref=0),string '的羊駝' (length=9)

還挺好理解的,對於數組來看是一個整體,對於內部kv來看又是分別獨立的整體,各自都維護着一套zval的refount和is_ref。

5.銷燬變量

<?php
    $name = "咖啡色的羊駝";
    $temp_name = $name;
    xdebug_debug_zval('name');
    unset($temp_name);
    xdebug_debug_zval('name');

會得到:

name:(refcount=2, is_ref=0),string '咖啡色的羊駝' (length=18)
name:(refcount=1, is_ref=0),string '咖啡色的羊駝' (length=18)

refcount計數減1,說明unset並非一定會釋放內存,當有兩個變量指向的時候,並非會釋放變量佔用的內存,只是refcount減1.

  • php的內存管理機制

知道了zval是怎麼一回事,接下來看看如何通過php直觀看到內存管理的機制是怎麼樣的。

1.外在的內存變化

先來一段代碼:

<?php
    //獲取內存方法,加上true返回實際內存,不加則返回表現內存
    var_dump(memory_get_usage());
    $name = "咖啡色的羊駝";
    var_dump(memory_get_usage());
    unset($name);
    var_dump(memory_get_usage());

會得到:

int 1593248
int 1593384
int 1593248

大致過程:定義變量->內存增加->清除變量->內存恢復

2.潛在的內存變化

當執行:

$name = "咖啡色的羊駝";

時候,內存的分配做了兩件事情:1.爲變量名分配內存,存入符號表 2.爲變量值分配內存

再來看代碼:

<?php

    var_dump(memory_get_usage());
    for($i=0;$i<100;$i++)
    {
        $a = "test".$i;
        $$a = "hello";    
	}
	var_dump(memory_get_usage());
	for($i=0;$i<100;$i++)
	{
	    $a = "test".$i;
 		unset($$a);    
    }
    var_dump(memory_get_usage());

會得到:

int 1596864
int 1612080
int 1597680

內存沒有全部回收回來。

對於php的核心結構Hashtable來說,由於未知性,定義的時候不可能一次性分配足夠多的內存塊。所以初始化的時候只會分配一小塊,等不夠的時候在進行擴容,而Hashtable只擴容不減少,所以就出現了上述的情況:當存入100個變量的時候,符號表不夠用了就進行一次擴容,當unset的時候只釋放了”爲變量值分配內存”,而“爲變量名分配內存”是在符號表的,符號表並沒有縮小,所以沒收回來的內存是被符號表佔去了。

3.潛在的內存申請與釋放設計

php和c語言一樣,也是需要進行申請內存的,只不過這些操作作者都封裝到底層了,php使用者無感知而已。

php的內存申請小設計

php並非簡單的向os申請內存,而是會申請一大塊內存,把其中一部分分給申請者,這樣當再有邏輯來申請內存的時候,就不需要向os申請了,避免了頻繁調用。當內存不夠的時候纔會再次申請

php的內存釋放小設計

當釋放內存的時候,php並非會把內存還給os,而是把內存軌道自己維護的空閒內存列表,以便重複利用, php中垃圾是如何定義的?

準確地說,判斷是否爲垃圾,主要看有沒有變量名指向變量容器zval,如果沒有則認爲是垃圾,需要釋放。

打個比方:

<?php
    $name = "咖啡色的羊駝";
    // todo other things

當定義name的時候,處理完字符串準備做其他事情的時候,對於我們來說name就是可以回收的垃圾了,然而對於引擎來說,$name還是實打實存在的refcount也還是1,所以就不是垃圾,不能回收。當調用unset的時候,也並不一定引擎會認爲它是一個垃圾而進行回收,主要還是看refcount是不是真的變爲0了。

  • 老版本php中如何產生內存泄漏垃圾?

產生內存泄漏主要真兇:環形引用。
現在來造一個環形引用的場景:

<?php
    $a = ['one'];
    $a[] = &$a;
    xdebug_debug_zval('a');

得到:

a:
(refcount=2, is_ref=1),
array (size=2)
  0 => (refcount=1, is_ref=0),string 'one' (length=3)
  1 => (refcount=2, is_ref=1),
        &array<

這樣$a數組就有了兩個元素,一個索引爲0,值爲one字符串,另一個索引爲1,爲$a自身的引用。

圖1

此時刪掉$a:

<?php
    $a = ['one'];
    $a[] = &$a;
    unset($a);

在這裏插入圖片描述
如果在小於php5.3的版本就會出現一個問題:$a已經不在符號表了,沒有變量再指向此zval容器,用戶已無法訪問,但是由於數組的refcount變爲1而不是0,導致此部分內存不能被回收從而產生了內存泄漏。

  • 5.3版本以後php是如何處理垃圾內存的?

1.判斷處理過程

爲解決環形引用導致的垃圾,產生了新的GC算法,遵守以下幾個基本準則:

1.如果一個zval的refcount增加,那麼此zval還在使用,不屬於垃圾

2.如果一個zval的refcount減少到0, 那麼zval可以被釋放掉,不屬於垃圾

3.如果一個zval的refcount減少之後大於0,那麼此zval還不能被釋放,此zval可能成爲一個垃圾

即對此zval中的每個元素進行一次refcount減1操作,操作完成之後,如果zval的refcount=0,那麼這個zval就是一個垃圾

引用php官方手冊的配圖:

圖3

A:爲了避免每次變量的refcount減少的時候都調用GC的算法進行垃圾判斷,此算法會先把所有前面準則3情況下的zval節點放入一個節點(root)緩衝區(root buffer),並且將這些zval節點標記成紫色,同時算法必須確保每一個zval節點在緩衝區中之出現一次。當緩衝區被節點塞滿的時候,GC纔開始開始對緩衝區中的zval節點進行垃圾判斷。

B:當緩衝區滿了之後,算法以深度優先對每一個節點所包含的zval進行減1操作,爲了確保不會對同一個zval的refcount重複執行減1操作,一旦zval的refcount減1之後會將zval標記成灰色。需要強調的是,這個步驟中,起初節點zval本身不做減1操作,但是如果節點zval中包含的zval又指向了節點zval(環形引用),那麼這個時候需要對節點zval進行減1操作。

C:算法再次以深度優先判斷每一個節點包含的zval的值,如果zval的refcount等於0,那麼將其標記成白色(代表垃圾),如果zval的refcount大於0,那麼將對此zval以及其包含的zval進行refcount加1操作,這個是對非垃圾的還原操作,同時將這些zval的顏色變成黑色(zval的默認顏色屬性)

D:遍歷zval節點,將C中標記成白色的節點zval釋放掉。

例如:

<?php
    $a = ['one']; --- zval_a(將$a對應的zval,命名爲zval_a)
    $a[] = &$a; --- step1
    unset($a);  --- step2

爲進行unset之前(step1),進行算法計算,對這個數組中的所有元素(索引0和索引1)的zval的refcount進行減1操作,由於索引1對應的就是zval_a,所以這個時候zval_a的refcount應該變成了1,這樣說明zval_a不是一個垃圾不進行回收。

當執行unset的時候(step2),進行算法計算,由於環形引用,上文得出會有垃圾的結構體,zval_a的refcount是1(zval_a中的索引1指向zval_a),用算法對數組中的所有元素(索引0和索引1)的zval的refcount進行減1操作,這樣zval_a的refcount就會變成0,於是就認爲zval_a是一個需要回收的垃圾。

算法總的套路:對於一個包含環形引用的數組,對數組中包含的每個元素的zval進行減1操作,之後如果發現數組自身的zval的refcount變成了0,那麼可以判斷這個數組是一個垃圾。

  • 算法優化配置

可能會發現,每次都進行這樣的操作好像會影響性能,是的,php做事情套路都是走批量的原則。

申請內存也是申請一大塊,僅使用當前的一小部分剩下的等下回再用,避免多次申請。

這個gc算法也是這樣,會有一個緩衝區的概念,等緩衝區滿了纔會一次性去給清掉。

開關配置

php.ini中設置 zend.enable_gc 項來開啓或則關閉GC。

緩衝區配置

緩衝區默認可以放10,000個節點,當緩衝區滿了纔會清理。可以通過修改Zend/zend_gc.c中的GC_ROOT_BUFFER_MAX_ENTRIES
來改變這個數值,需要重新編譯鏈接PHP

關鍵函數

gc_enable() : 開啓GC

gc_disable() : 關閉GC

gc_collect_cycles() : 在節點緩衝區未滿的情況下強制執行垃圾分析算法

  • 涉及到垃圾回收的知識點

1.unset函數

unset只是斷開一個變量到一塊內存區域的連接,同時將該內存區域的引用計數-1;內存是否回收主要還是看refount是否到0了,以及gc算法判斷。

2.= null 操作

a=null是直接將,a 指向的數據結構置空,同時將其引用計數歸0。

3.腳本執行結束

腳本執行結束,該腳本中使用的所有內存都會被釋放,不論是否有引用環。

本文整理自 http://blog.csdn.net/u011957758/article/details/76864400

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