首頁  >  文章  >  後端開發  >  PHP核心的儲存機制(分離/改變)的圖文程式碼詳解

PHP核心的儲存機制(分離/改變)的圖文程式碼詳解

黄舟
黄舟原創
2017-03-09 09:27:091313瀏覽

前言:

大部分程式設計師看部落格可能不是太喜歡看漢字比較多的文章哈,但本文確實介紹以漢字為主描述,耐心看完,對大部分人來說一定會有收穫!

或許你知道,或許你不知道,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中的變數包含四種複合型別(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(&#39;one&#39;);  
$a[] = &$a;  
xdebug_debug_zval(&#39;a&#39;);


debug_zval_dump打印出zval的结构是:

a: (refcount=2, is_ref=1)=array (
    0 => (refcount=1, is_ref=0)=&#39;one&#39;, 
    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)=&#39;one&#39;, 
    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://www.php.cn/

    垃圾回收機制是最早在Lisp中被提出,關於更多垃圾回收的資訊.


以上是PHP核心的儲存機制(分離/改變)的圖文程式碼詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn