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進階架構師>>>實戰視頻、大廠面試文檔免費獲取