PHP中垃圾回收機制詳解

語言: CN / TW / HK

前言

平時很多人說到的gc,就是垃圾回收器,全稱Garbage Collection。

早期版本,準確地說是5.3之前(不包括5.3)的垃圾回收機制,是沒有專門的垃圾回收器的。只是簡單的判斷了一下變數的zval的refcount是否為0,是的話就釋放否則不釋放直至程序結束。

乍一看確實沒毛病啊,然而其中隱藏著變數記憶體溢位的風險:http://bugs.php.net/bug.php?id=33595 ,無法回收的記憶體造成了記憶體洩漏,所以PHP5.3出現了專門負責清理垃圾資料、防止記憶體洩漏的GC。

php引用計數基本知識點

不準確但卻通俗的說:
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來標記是否存在引用的情況。

陣列型的變數

<?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。

銷燬變數

<?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直觀看到記憶體管理的機制是怎麼樣的。

外在的記憶體變化

<?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

大致過程:定義變數->記憶體增加->清除變數->記憶體恢復

潛在的記憶體變化

當執行 時候,記憶體的分配做了兩件事情:

1.為變數名分配記憶體,存入符號表

2.為變數值分配記憶體

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

再來看程式碼:

<?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的時候只釋放了”為變數值分配記憶體”,而“為變數名分配記憶體”是在符號表的,符號表並沒有縮小,所以沒收回來的記憶體是被符號表佔去了。
 

潛在的記憶體申請與釋放設計

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

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

當釋放記憶體的時候,php並非會把記憶體還給os,而是把記憶體軌道自己維護的空閒記憶體列表,以便重複利用。

以上內容希望幫助到大家, 很多PHPer在進階的時候總會遇到一些問題和瓶頸,業務程式碼寫多了沒有方向感,不知道該從那裡入手去提升,對此我整理了一些資料,包括但不限於:分散式架構、高可擴充套件、高效能、高併發、伺服器效能調優、TP6,laravel,Redis,Swoole、Swoft、Kafka、Mysql優化、shell指令碼、Docker、微服務、Nginx等多個知識點高階進階乾貨需要的可以免費分享給大家 ,需要戳這裡     PHP進階架構師>>>實戰視訊、大廠面試文件免費獲取