PHP中的unset方法究竟能否釋放內(nèi)存

1.有人說

今天寫著寫著代碼用到了unset所以想整理一下.

有的人說PHP的unset并不真正釋放內(nèi)存, 有的說, PHP的unset只是在釋放大變量(大量字符串, 大數(shù)組)的時候才會真正free內(nèi)存, 更有人說, 在PHP層面討論內(nèi)存是沒有意義的.
也有人說:
unset()函數(shù)只能在變量值占用內(nèi)存空間超過256字節(jié)時才會釋放內(nèi)存空間。
只有當指向該變量的所有變量(如引用變量)都被銷毀后,才會釋放內(nèi)存。

2.辟謠,測試環(huán)境php7.2

第一個例子

$s=str_repeat('1',255); //產(chǎn)生由255個1組成的字符串
$m=memory_get_usage(); //獲取當前占用內(nèi)存
unset($s);
$mm=memory_get_usage(); //unset()后再查看當前占用內(nèi)存
echo $m-$mm;

結(jié)果

?  sites php index.php
320

第二個例子,和第一個例子一樣,只不過產(chǎn)生10個

$s=str_repeat('1',10); //產(chǎn)生由255個1組成的字符串
$m=memory_get_usage(); //獲取當前占用內(nèi)存
unset($s);
$mm=memory_get_usage(); //unset()后再查看當前占用內(nèi)存
echo $m-$mm;

結(jié)果

?  sites php index.php
48

說明unset釋放了內(nèi)存,也并沒有256字節(jié)的限制。

第三個例子

$s=str_repeat('1',256); //這和第二個例子完全相同
$p=&$s;
$m=memory_get_usage();
unset($s); //銷毀$s
$mm=memory_get_usage();
echo $p."\n";
echo $m-$mm;

結(jié)果

?  sites php index.php
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
0

說明這個例子并沒有釋放內(nèi)存。

3.php變量的內(nèi)存分配

簡介

首先我們要知道php的內(nèi)存分配是隱式的,并不像c語言那樣顯示調(diào)用內(nèi)存分配API就會分配內(nèi)存。
比如我們 定義變量:$i = 'How are you!';
隱式分配有兩個過程:1.為變量分配內(nèi)存,存入符號表。2 .為變量值分配內(nèi)存。
PHP是一個弱類型,動態(tài)的腳本語言。所謂弱類型,就是說PHP并不嚴格驗證變量類型(嚴格來講,PHP是一個中強類型語言,這部分內(nèi)容會在以后的文章中敘述),在申明一個變量的時候,并不需要顯示指明它保存的數(shù)據(jù)的類型:

  $var = 1; //int
  $var = "laruence"; //string
  $var = 1.0002; //float
  $var = array(); // array
  $var = new Exception('error'); //object;

動態(tài)語言,就是說,PHP的語言結(jié)構(gòu)在運行期是可以改變的,比如我們在運行期require一個函數(shù)定義文件,從而導致語言的函數(shù)表動態(tài)的改變。
所謂腳本語言,就是說,PHP并不是獨立運行的,要運行PHP我們需要PHP解析器。PHP的執(zhí)行是通過Zend engine(ZE, Zend引擎), ZE是用C編寫的,大家都知道C是一個強類型語言,也就是說,在C中所有的變量在它被聲明到最終銷毀,都只能保存一種類型的數(shù)據(jù)。 那么PHP是如何在ZE的基礎(chǔ)上實現(xiàn)弱類型的呢?

在PHP中,所有的變量都是用一個結(jié)構(gòu)-zval來保存的, 在Zend/zend.h中我們可以看到zval的定義:

typedef struct _zval_struct {
    zvalue_value value; // 值
    zend_uint refcount; // 賦值的次數(shù)
    zend_uchar type; // 存放類型
    zend_uchar is_ref; // 是否引用了0,1
  } zval;

首先介紹zvalue_value,其中zvalue_value是真正保存數(shù)據(jù)的關(guān)鍵部分,現(xiàn)在到了揭曉謎底的時候了,PHP是如何在ZE的基礎(chǔ)上實現(xiàn)弱類型的呢? 因為zvalue_value是個聯(lián)合體(union),

typedef union _zvalue_value {
    long lval;
    double dval;
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;
    zend_object_value obj;
} zvalue_value;

那么這個結(jié)構(gòu)是如何儲存PHP中的多種類型的呢?
PHP中常見的變量類型有:

1. 整型/浮點/長整型/bool值 等等
2. 字符串
3. 數(shù)組/關(guān)聯(lián)數(shù)組
4. 對象
5. 資源

PHP根據(jù)zval中的type字段來儲存一個變量的真正類型,然后根據(jù)type來選擇如何獲取zvalue_value的值,比如對于整型和bool值:

 zval.type = IS_LONG;//整形
 zval.type = IS_BOOL;//布爾值

就去取zval.value.lval,對于bool值來說lval∈(0|1);
如果是雙精度,或者float則會去取zval.value的dval。
而如果是字符串,那么:這個時候,就會取:
zval.value.str
而這個也是個結(jié)構(gòu),存有C分格的字符串和字符串的長度。
而對于數(shù)組和對象,則type分別對應IS_ARRAY, IS_OBJECT, 相對應的則分別取zval.value.ht和obj
比較特別的是資源,在PHP中,資源是個很特別的變量,任何不屬于PHP內(nèi)建的變量類型的變量,都會被看作成資源來進行保存,比如,數(shù)據(jù)庫句柄,打開的文件句柄等等。 對于資源:

type = IS_RESOURCE

這個時候,會去取zval.value.lval, 此時的lval是個整型的指示器, 然后PHP會再根據(jù)這個指示器在PHP內(nèi)建的一個資源列表中查詢相對應的資源,目前,你只要知道此時的lval就好像是對應于資源鏈表的偏移值。

 ZEND_FETCH_RESOURCE(con, type, zval *, default, resource_name, resource_type);

借用這樣的機制,PHP就實現(xiàn)了弱類型,因為對于ZE的來說,它所面對的永遠都是同一種類型,那就是zval。

php就是這樣實現(xiàn)了弱類型,那變量內(nèi)存究竟是如何分配的呢?
ZE是如何把我的變量var和內(nèi)部結(jié)構(gòu)zval聯(lián)系起來的呢?
PHP內(nèi)部都是使用zval來表示變量的,但是對于上面的腳本,我們的變量是有名字的, var。而zval中并沒有相應的字段來體現(xiàn)變量名。

如果你想到了PHP內(nèi)部一定有一個機制,來實現(xiàn)變量名到zval的映射。
在PHP中,所有的變量都會存儲在一個數(shù)組中(確切的說是hash table), 并且,PHP也是通過不同的數(shù)組來實現(xiàn)變量的作用域的。
當你創(chuàng)建一個變量的時候,PHP會為這個變量分配一個zval,填入相應的變量值,然后將這個變量的名字,和指向這個zval的指針填入一個數(shù)組中。然后,當你獲取這個變量的時候,PHP會通過查找這個數(shù)組,獲得對應的zval。

查看_zend_executor_globals結(jié)構(gòu)(這個結(jié)構(gòu)在PHP的執(zhí)行器保存一些執(zhí)行相關(guān)的上下文信息)

struct _zend_executor_globals {
 
     ....
    HashTable *active_symbol_table;/*活動符號表*/
    HashTable symbol_table;     /*全局符號表*/
 
    HashTable included_files;   
 
    jmp_buf *bailout;
    int error_reporting;
     .....
}

其中,全局符號表,保存了在頂層作用域(就是不在任何函數(shù),對象內(nèi))的變量。每當調(diào)用一個函數(shù)(對象的方法)的時候,就會為這個函數(shù)創(chuàng)建一個活動符號表,所有在這個函數(shù)內(nèi)定義的變量,都會保存在這個活動符號表中。
對,這就是PHP的變量作用域的實現(xiàn)方式! 舉個列子

  <?php
     $var = "I am in the global symbol table";
    function sample($para){
        $var = "I am in the active symbol table";
          echo $var;
      }
    sample($var);
    echo $var;
  ?>

在函數(shù)sample外面的變量$var,它會被填入全局符號表中,與他對應的有一個zval指針,這個zval保存了一個字符串”I am in the global symbol table”.
函數(shù)內(nèi)的$var, 它會被填入屬于函數(shù)sample的活動符號表中,一樣的,與他對應的zval中,保存著字符串”I am in the active symbol table
比較特殊的,就是函數(shù)sample的參數(shù)para了,這個para是保存在sample的活動符號表的,但是與他對應的zval指針,會指向一個保存一份全局變量$var的copy的zval(嚴格來講不是copy,是引用,這個涉及到變量的copy on write機制,我會在以后介紹)。

我們來回顧:

struct _zval_struct {
        /* Variable information */
        zvalue_value value;             /* value */
        zend_uint refcount;
        zend_uchar type;        /* active type */
        zend_uchar is_ref;
};

其中的refcount和is_ref字段我們一直都沒有介紹過,我們知道PHP是一個長時間運行的服務器端的腳本解釋器。那么對于它來說,效率和資源占用率是一個很重要的衡量標準,也就是說,PHP必須盡量介紹內(nèi)存占用率,考慮下面這段代碼:

<?php
   $var = "How are you";
   $var_dup = $var;
   unset($var);
?>

第一行代碼創(chuàng)建了一個字符串變量,申請了一個大小為12字節(jié)的內(nèi)存,保存了字符串”how are you”和一個NULL(\0)的結(jié)尾。
第二行定義了一個新的字符串變量,并將變量var的值”復制”給這個新的變量。
第三行unset了變量var
這樣的代碼在我們平時的腳本中是很常見的,如果PHP對于每一個變量賦值都重新分配內(nèi)存,copy數(shù)據(jù)的話,那么上面的這段代碼公要申請18個字節(jié)的內(nèi)存空間,而我們也很容易的看出來,上面的代碼其實根本沒有必要申請倆份空間,呵呵,PHP的開發(fā)者也看出來了:

PHP中的變量是用一個存儲在symbol_table中的符號名,對應一個zval來實現(xiàn)的,比如對于上面的第一行代碼,會在symbol_table中存儲一個值”var”, 對應的有一個指針指向一個zval結(jié)構(gòu),變量值”how are you”保存在這個zval中,所以不難想象,對于上面的代碼來說,我們完全可以讓”var”和”var_dup”對應的指針都指向同一個zval就可以了。PHP也是這樣做的,這個時候就需要介紹我們之前一直沒有介紹過的zval結(jié)構(gòu)中的refcount字段了。
refcount,顧名思義,記錄了當前的zval被引用的計數(shù)。
比如對于代碼:

<?php $var = 1; $var_dup = $var; ?>

第一行,創(chuàng)建了一個整形變量,變量值是1。 此時保存整形1的這個zval的refcount為1。
第二行,創(chuàng)建了一個新的整形變量,變量也指向剛才創(chuàng)建的zval,并將這個zval的refcount加1,此時這個zval的refcount為2。

現(xiàn)在我們回頭看文章開頭的代碼, 當執(zhí)行了最后一行unset($var)以后,會發(fā)生什么呢? 對,既是refcount減1,上代碼:

<?php
   $var = "how are you";
   $var_dup = $var;
   unset($var);
?>

這就是PHP的copy on write機制:
PHP在修改一個變量以前,會首先查看這個變量的refcount,如果refcount大于1,PHP就會執(zhí)行一個分離的例程, 對于上面的代碼,當執(zhí)行到第三行的時候,PHP發(fā)現(xiàn)var指向的zval的refcount大于1,那么PHP就會復制一個新的zval出來,將原zval的refcount減1,并修改symbol_table,使得var和$var_dup分離(Separation)。這個機制就是所謂的copy on write(寫時復制)。

現(xiàn)在我們知道,當使用變量復制的時候 ,PHP內(nèi)部并不是真正的復制,而是采用指向相同的結(jié)構(gòu)來盡量節(jié)約開銷。那么,對于PHP中的引用,那又是如何實現(xiàn)呢?

<?php
   $var = "how are you";
   $var_ref = &$var;
   $var_ref = 1;
?>

這段代碼結(jié)束以后,var也會被間接的修改為1,這個過程稱作(change on write:寫時改變)。那么ZE是怎么知道,這次的復制是不需要Separation的呢? 這個時候就要用到zval中的is_ref字段了: 對于上面的代碼,當?shù)诙袌?zhí)行以后,var所代表的zval的refcount變?yōu)?,并且同時置is_ref為1。
到第三行的時候,PHP先檢查var_ref代表的zval的is_ref字段,如果為1,則不分離,大體邏輯示意如下:

if((*val)->is_ref || (*val)->refcount<2){
          //不執(zhí)行Separation
        ... ;//process
  }

但是:

<?php
   $var = "laruence";
   $var_dup = $var;
   $var_ref = &$var;
?>

對于上面的代碼,存在一對copy on write的變量var和var_dup, 又有一對change on write機制的變量對var和var_ref,這個情況又是如何運作的呢?
當?shù)诙袌?zhí)行的時候,和前面講過的一樣,var_dup 和var 指向相同的zval, refcount為2.
當執(zhí)行第三行的時候,PHP發(fā)現(xiàn)要操作的zval的refcount大于1,則,PHP會執(zhí)行Separation, 將var_dup分離出去,并將var和$var_ref做change on write關(guān)聯(lián)。也就是,refcount=2, is_ref=1;
所以不要懷疑unset的釋放內(nèi)存的能力,但這個釋放不是C編程意義上的釋放, 不是交回給OS.對于PHP來說, 它自身提供了一套和C語言對內(nèi)存分配相似的內(nèi)存管理API:

emalloc(size_t size);
efree(void *ptr);
ecalloc(size_t nmemb, size_t size);
erealloc(void *ptr, size_t size);
estrdup(const char *s);
estrndup(const char *s, unsigned int length);

這些API和C的API意義對應, 在PHP內(nèi)部都是通過這些API來管理內(nèi)存的.

當我們調(diào)用emalloc申請內(nèi)存的時候, PHP并不是簡單的向OS要內(nèi)存, 而是會像OS要一個大塊的內(nèi)存, 然后把其中的一塊分配給申請者, 這樣當再有邏輯來申請內(nèi)存的時候, 就不再需要向OS申請內(nèi)存了, 避免了頻繁的系統(tǒng)調(diào)用.

同樣的, 在我們調(diào)用efree釋放內(nèi)存的時候, PHP也不會把內(nèi)存還給OS, 而會把這塊內(nèi)存, 歸入自己維護的空閑內(nèi)存列表. 而對于小塊內(nèi)存來說, 更可能的是, 把它放到內(nèi)存緩存列表中去(后記, 某些版本的PHP, 比如我驗證過的PHP5.2.4, 5.2.6, 5.2.8, 在調(diào)用get_memory_usage()的時候, 不會減去內(nèi)存緩存列表中的可用內(nèi)存塊大小, 導致看起來, unset以后內(nèi)存不變,)但是 php5.5驗證,會減少內(nèi)存。.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容