PHP內核的存儲機制(分離/改變)


前言:大部分程序員看博客可能不是太喜歡看漢字比較多的文章哈,但本文確實介紹以漢字為主描述,耐心看完,對大部分人來說肯定會有收獲!
或許你知道,或許你不知道,PHP是一個弱類型,動態的腳本語言。所謂弱類型,就是說PHP並不嚴格驗證變量類型(嚴格來講,PHP是一個中強類型語言),在申明一個變量的時候,並不需要顯示指明它保存的數據的類型。比如:$a = 1; (整形) $a ="1";(字符串)

一直使用PHP,但它究竟什么,底層是怎么實現才成就了PHP這樣方便快捷的弱類型語言。 

最近也查閱了很多書籍,還有相關博客資料,了解到了許多關於PHP內核的一些機制。

php簡單的理解就是一個c語言的類庫,你去php.net 下面下載一下它的源代碼就會發現,首先php的內核是zend engine ,它是一個用c語言寫的函數庫,用於處理底層的函數管理,內存管理,類管理,和變量管理。在內核上面,他們寫了很多擴展,這些擴展大多數都是獨立的。用操作系統來比喻的話,zend engine 就是一個操作系統,然后官方提供了很多“應用程序”,只是這個“應用程序” 不是media play 而是 mysql , libxml,dom。當然,你也可以根據zend engine 的api 開發自己的擴展。



下邊開始介紹下PHP變量在內核中的存儲機制。

PHP是若類型語言,也就是說一個PHP變量可以保存任何的數據類型。但PHP是使用C語言編寫的,而C語言是強類型語言是吧,每個變量都會有固定的類型,(一顆通過強類型轉變,不過有可能出現問題),那在Zend引擎中如何做到一個變量保存任何數據類型?下邊請看它存儲結構體。打開Zend/zend.h頭文件,會發現下列結構體Zval

1.zval結構

 typedef struct _zval_struct zval;
 typedef union _zvalue_value {    long lval;      /* long value */    double dval;    /* double value */    struct {    char *val; //4字節    int len;   //4字節    } str;    HashTable *ht;    /* hash table value */    zend_object_value obj; } zvalue_value;

 struct _zval_struct {
/* Variable information */
zvalue_value value; /* 變量值保存在這里 12字節*/
zend_uint refcount;//4字節,變量引用計數器
zend_uchar type; /* active type變量類型 1字節*/
zend_uchar is_ref;//是否變量被&引用,0表示非引用,1表示引用,1字節
};


2.zend_uchar type
PHP中的變量包括四種標量類型(bool,int,float,string),兩種復合類型(array, object)和兩種特殊的類型(resource 和NULL)。在zend內部,這些類型對應於下面的宏(代碼位置 phpsrc/Zend/zend.h) Zend根據type值來決定訪問value的哪個成員,可用值如下:


3
.zend_uint refcount__gc  


該值實際上是一個計數器,用來保存有多少變量(或者符號,symbols,所有的符號都存在符號表(symble table)中, 不同的作用域使用不同的符號表,關於這一點,我們之后會論述)指向該zval。在變量生成時,其refcount=1,典型的賦值操作如$a = $b會令zval的refcount加1,而unset操作會相應的減1。在PHP5.3之前,使用引用計數的機制來實現GC,如果一個zval的refcount較少到0,那么Zend引擎會認為沒有任何變量指向該zval,因此會釋放該zval所占的內存空間。但,事情有時並不會那么簡單。后面我們會看到,單純的引用計數機制無法GC掉循環引用的zval(詳見后舉例3),即使指向該zval的變量已經被unset,從而導致了內存泄露(Memory Leak)。


4.is_ref__gc
. 這個字段用於標記變量是否是引用變量。對於普通的變量,該值為0,而對於引用型的變量,該值為1。這個變量會影響zval的共享、分離等。關於這點,我們之后會有論述。

正如名字所示,ref_count__gc和is_ref__gc是PHP的GC機制所需的很重要的兩個字段,這兩個字段的值,可以通過xdebug等調試工具查看。


下面我們圍繞zval,展開敘述,PHP變量到底是怎么個存儲機制。

Xdebug的安裝我在前邊PHPstorm Xdebug調試也介紹過,這里不贅述,請看: phpstorm+Xdebug斷點調試PHP

安裝成功后,你的腳本中,可以通過xdebug_debug_zval打印Zval的信息,用法:

 $var = 1;
debug_zval_dump($var);
$var_dup = $var;
debug_zval_dump($var);


實例一:

    $a = 1;
$b = $a;
$c = $b;
$d = &$c; // 在一堆非引用賦值中,插入一個引用



   整個過程圖示如下:

---------------------------------------------------------

實例二:

   $a = 1;
$b = &$a;
$c = &$b;
$d = $c; // 在一堆引用賦值中,插入一個非引用


   整個過程圖示如下:





通過實例一、二,展現了,這就是PHP的copy on write寫時分離機制change on write寫時改變機制

過程:

PHP在修改一個變量以前,會首先查看這個變量的refcount,如果refcount大於1,PHP就會執行一個分離的例程, 

     對於上面的實例一代碼,當執行到第四行的時候,PHP發現$c指向的zval的refcount大於1,那么PHP就會復制一個新的zval出來,將原zval的refcount減1,並修改symbol_table,使得$a,$b和$c分離(Separation)。這個機制就是所謂的copy on write(寫時復制/寫時分離)。把$d指向的新zval的is_ref的值 == 1 ,這個機制叫做change on write(寫時改變)


結論:

分離指的是:分離兩個變量存儲的zval的位置,讓分開不指向同一個空間! (那如何判定是否要分離呢,依據是什么?見下邊)

改變指的是,有&引用賦值時,要把新開辟的zval 的 is_ref 賦值為1


判定是否分離的條件:如果is_ref =1 或recount == 1,則不分離
if((*val)->is_ref || (*val)->refcount<2){
//不執行Separation
... ;//process
}




---------------------------------------------------------------------------------------------------

實例三:(內存是如何泄漏的)

數組變量與普通變量生成的zval非常類似,但也有很大不同

舉例:
$a = $array('one');  
$a[] = &$a;
xdebug_debug_zval('a');

debug_zval_dump打印出zval的結構是:
a: (refcount=2, is_ref=1)=array (
0 => (refcount=1, is_ref=0)='one',
1 => (refcount=2, is_ref=1)=...
)

上述輸出中,…表示指向原始數組,因而這是一個循環的引用。如下圖所示:






現在,我們對$a執行unset操作,這會在symbol table中刪除相應的symbol,同時,zval的refcount減1(之前為2),也就是說,現在的zval應該是這樣的結構:
unset($a);
(refcount=1, is_ref=1)=array (
0 => (refcount=1, is_ref=0)='one',
1 => (refcount=1, is_ref=1)=...
)

(應該ref_count=1)

(unset,其實就是打斷$a在 符號表(symble table) 與zval 的一個指針映射關系。)

這時,不幸的事情發生了!

  Unset之后,雖然沒有變量指向該zval,但是該zval卻不能被GC(指PHP5.3之前的單純引用計數機制的GC)清理掉,$a 被釋放,但是$a里的$a[1]也指向了該zval,它沒有被釋放,導致zval的refcount均大於0。這樣,這些zval實際上會一直存在內存中,直到請求結束(參考SAPI的生命周期)。在此之前,這些zval占據的內存不能被使用,便白白浪費了,換句話說,無法釋放的內存導致了內存泄露

  如果這種內存泄露僅僅發生了一次或者少數幾次,倒也還好,但如果是成千上萬次的內存泄露,便是很大的問題了。尤其在長時間運行的腳本中(例如守護程序,一直在后台執行不會中斷),由於無法回收內存,最終會導致系統“再無內存可用”,所以說,一定要避免這種操作。


垃圾回收機制:

1.php原來是通過引用計數器來實現內存回收,也就是是多個php變量可能會引用同一份內存,這種情況unset掉其中一個是不會釋放內存的;
    例如:$a
= 1; $b = $a; unset($a);//$a開辟的內存不會回收

2.離開了變量的作用域后變量所占用的內存就會被自動清理(不包含靜態變量,靜態變量在腳本加載時創建,在腳本結束時釋放),
    如函數或方法內的局部變量,對這些局部變量進行unset在函數外來看內存也是沒有減少的。3.引用計數有個缺陷,就是當循環引用出現時,計數器沒法清0,內存占用會持續到頁面訪問結束    對於這個問題PHP5.3中增加了垃圾回收機制。具體可以查閱文檔:http://php.net/manual/zh/features.gc.php    垃圾回收機制就是最早在Lisp中被提出,關於更多垃圾回收的信息.    參見維基百科:https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)


本文參考文獻:

  1. 鳥哥的深入變量引用/分離  http://www.laruence.com/2008/09/19/520.html



本文鏈接地址:http://blog.csdn.net/ty_hf/article/details/51057954
啊~終於寫完了,清明三天假期也就這么過去了,今天4月4號,明天開始准備上班啦!~奮斗



注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com