PHP源碼分析(基本變量、垃圾回收)

小而巧的zval

擴充:

結構體: 比如 

              struct test  {  char a //1  int b//4 long c //8  } 

               總共佔了2*8=16字節 

              因爲結構體對齊,雖然浪費字節,但是得益於內存對齊,存取速度會更快

聯合體:比如 union{

              char a; //1   int b;4 long c ; //8

             }

           結果:(gdb) p sizeof(union test)   $1=8 

          它複用了同一塊內存,a、b和c公用同一塊內存,修改a,也會修改b和c的值,同時可以知道聯合體的大小爲其最大成員的大小

棧區:存儲參數值、局部變量,維護函數調用關係,棧的變量是局部的,隨着局部空間的銷燬而銷燬,由系統負責

堆區:動態內存區域,隨時申請和釋放,程序自身要對內存泄露負責,堆上面的變量可以提供全局訪問,需要自行處理其生命週期

 

  zval可以表示PHP中任意一個變量

struct_zval_struct{
   zend_value  value;
   union  u1;
   union  u2;
}


typedef union_zend_value{
   zend_long  lval;  //整形
   double     dval;  //浮點型
   zend_refcounted *counted 
   zend_string  *str; //字符串
   zend_array   *arr; //數組 ->hashtable
   zend_object  *obj; //對象
   zend_resource *res; //資源類型
   zend_reference *ref; //引用類型
   zend_ast_ref   *ast; //抽象語法樹
   zval  *zv;  //還可以指向一個zval
   void  *ptr; //不確定類型
   zend_class_entry *ce;//類
   zend_function  *func;

}zend_value;

源碼圖:文件zand_types.h

u1結構體:

       type 定義了變量的類型 

       type_flags 是變量類型特有的標記,可以表示常量、不可變的類型(存在共享內存中的數組)、需要引入計數的類型、可能包括循環引用的類型(is_array,is_object)、可被複制的類型

      const_flags:是常量類型特有的標記,0變量 2常量

      reserved: 是保留字段

  在PHP7中,通過value和u1已經可以表示任何類型,並記錄一些類型的屬性。另外還有一個u2其實是增加輔助字段。

u2結構體:

    next:是主要解決hash衝突的,記錄衝突的下一個元素位置

    cache_slot:主要是做運行時緩存的,在執行函數的時候優先區緩存查找,若緩存沒有,會在全局function 表中查找

    lineno 標記了哪一行,一般用作AST抽象語法樹

    num_args:代表函數調用輸入參數的個數

    fe_pos:代表我們foreach的時候位置,每foreach一次這個值+1,當再次調用foreach對數組進行遍歷,會首先對這個指針重置。

    fe_iter_idx:也在foreach中使用,代表遊標索引的位置

    access_flags:主要用在類裏面比如寫代碼用到的public、protected等

     property_guard:防止類中魔術方法的循環引用,在get和set中會用到

其中zval裏面有三個變量,他們都是聯合體,   value裏面有各種類型的指針,它的類型判斷是由u1裏面的type來判斷的,根據type來取裏面不同的值,比如type裏面是IS_LONG整形,可以直接取zend_value裏面的lval,就得到值了,如果type是string,我們取*str,它是一個指向zend_string的指針

變量都挨在一起放在同一段內存中,每一個都佔了16字節

 在PHP5中,所有的變量都是在堆中申請,但是對於臨時變量是沒有必要的,而PHP7進行了優化,這種臨時變量直接在棧中申請。

 

1、從源碼中我們看到雖然說PHP是弱語言類型,但是在真正的底層實現還是區分類型的,爲什麼需要區分類型呢?

           如果知道一個變量想知道它的長度的話有這麼幾種方法,一個是專門用一個長度的字段來記錄長度比如我們字符串,數組的話我們有一個長度的字段,其他的我們用類型這個字段,而類型天然有長度這個字段,一個類型隱世的包含它的長度,比如我們定義 $a=1雖然我們沒有定義類型,但是底層解析會把他定義爲整形,會用到zend_value 的lval

 2、 如何區分類型:看u1裏面的結構體v的type,這個type就是代表不同的類型,類型定義了有IS_FALSE、IS_TRUE、IS_LONG、IS_ARRAY、IS_OBJECT等,另外還有一個type_info,可以快速的取出上面的四個char的值

 

Zend_string  字符串

 

 注:_zend_string頭部維護了gc的信息,並且冗餘了hash值h,這個操作據說爲PHP7提高了5%的性能,避免在數組操作中hash值的重複計算

gc對應一個結構體,主要是進行垃圾回收的,refcount爲引用計數

h 爲這個字符串對應的hash值,後面會用在數組裏面

len和val[1]:是二進制安全的,不像c語言中如果/0就會被截斷,兩個就可以表示一個字符串,其中gc和h主要用作垃圾回收和以空間換時間做hash運算時存儲的一個h值

寫時複製:如果是整形或者其他簡單的類型,用zval的16個字節就可以表示,所以是直接賦值的,比如zend_string,$a='string',$b=$a,他們的*str指向同一個zend_string,使用gc裏面的refcount+1,當修改b的時候進行copy一份出來進行修改

對於7.1.0的話 refcount=0,flags=2常量  變量的話   refcount=1,flags=0,並且字符串是寫時複製的,當沒有修改前的話,他們的*str都指向同一個zend_string,修改後refcount-1,並且分配一個新的地址

 

引用類型:

 在zend_value裏面 

 zend_reference    *ref  對應 IS_REFERENCE 

   當把$b=&$a的的時候,a的type也變成10,引用類型了,都是同一個地址

$a='hello' //$a->zend_string(refcount=1,val)
$b=$a;//$b,$a->zend_string(refcount=2.,val)
$c=&$b; //$a->zend_string(refcount=2,val)
       //$c,$b->zval(type=IS_REFERENCE,refcount=2)->zend_string(refcount=2,val)

   zend_reference記錄着gc信息的zend_refcounted_h結構體和zval結構體組成。由zval儲存實際的值,zend_refcounted_h結構體用來存儲引用計數的信息。在PHP 7中複雜類型的引用計數信息都記錄在自身頭部的gc裏面,zval沒有存儲引用計數的字段,所以增加了這種結構用來垃圾回收

複製和引用:

   當$a複製string的時候,$a對應的zval的類型爲IS_STRING,指向的是zend_string,當$b=&$a的時候,它們的類型都變成了zend_reference,它們指向同一個zend_reference,因爲zend_reference裏面有zval和gc,它的類型爲IS_STRING,指向真正的zend_string,所以zend_string結構體的引用計數不變,同時zend_reference的結構體引用計數變成2。這樣做的好處是原始的zend_string在內存中始終只有一份(避免了由於字符串的重複申請導致的內存浪費)。 當使用unset($b)時候,只是把b的zval的type類型改爲NULL,甚至ref地址的指針都沒有變,而a的zval的type還是爲10,類型爲ref沒有變,指向的zend_string也沒有變

   

 

Zend_array  數組

zend_array  還有個別名 HashTable

TableMask:計算索引值

*arDate:真正存儲的是key-value對

NumUsed:已經用過的空間

TableSize:代表的是arDate的大小,初始化的話大小爲8,當不夠用的話進行擴容8-16-32

NextFreeElement:當我們直接賦值沒有key的value

整個HashTable分爲了Bucket arr和hash arr

 

Bucket array是後面存arrData的下標一個個往上加的,0123,我寫進去的時候他們key一定是數字,這個arData前面有一個索引的數組,只用兩個位置,分別是-1和-2,這個是Bucke array

Hash array是算出來的hash值& nTableMask得到一個值

 

垃圾回收

 在PHP 7中複雜類型的引用計數信息都記錄在自身頭部的gc裏面,zval沒有存儲引用計數的字段,所以增加了這種結構用來垃圾回收,PHP7垃圾回收的實現方法是定期遍歷和標記若干存儲對象的數組,在通過算法將是垃圾的物理空間回收。

 在php中 除了 arrayobject類型的變量,其餘大部分是自動回收,在自動GC機制中,在zval斷開value的指向時如果發現refcount=0則直接釋放value,這時變量的回收時機,發生斷開的這兩種常見的情況是修改變量與函數返回時,修改變量會斷開原有value的指向,函數返回時會釋放所有的局部變量,也就是把所有局部變量的引用計數減一


$a = 1;
$b = $a;
xdebug_debug_zval('a');
$a =10;
xdebug_debug_zval('a');
unset($a);
xdebug_debug_zval('a');

結果

a:
(refcount=2, is_ref=0),int 1
a:
(refcount=1, is_ref=0),int 10
a: no such symbol

可以看到 當$a =10 的時候 涉及到 php的COW(copy-on-write)機制,$b 會複製一份原先的 $a ,解除了他們之間的引用關係,所以a的引用次數(refcount)減少爲1。

然後我們uset($a)之後 a的引用次數變爲0。這就會被認爲是垃圾變量,釋放空間。
 

還有一種情況是這個機制無法解決的,從而因變量無法回收導致內存始終得不到釋放,造成內存泄露,這種情況就是循環引用。也就是變量的內部引用了變量本身。

例如:

$a = [1];
$a[1] = &$a;
unset($a);

在 unset($a) 之前 $a 的類型爲引用類型

a:
(refcount=2, is_ref=1),
array (size=2)
  0 => (refcount=1, is_ref=0),int 1
  1 => (refcount=2, is_ref=1),
    &array<

unset($a) 之後,就變成這樣

這時候,我們unset操作時refcount 由2變爲1,所有的外部引用都斷開的時候,因爲有內部引用指向 $a,數組的refcount仍然大於0而得不到釋放

它就成了一個“孤兒”,在c語言中叫做野指針。在php中叫做循環引用。內存泄漏。想要銷燬變量的話,只能等 php腳本結束。
 

循環引用造成的內存泄漏

爲了清理這些垃圾,引入了兩個準則

   如果引用計數減少到零,所在變量容器將被清除(free),不屬於垃圾
   如果一個zval 的引用計數(refcount)減少後還大於0,那麼它可能會是一個垃圾。 

循環引用基本上只會出現在 數組和對象中,對象是因爲它的本身就是引用,針對第一種情況的話,垃圾回收期不會處理,只有第二種情況的話會將垃圾收集起來。

Object的情況是則是成員屬性引用對象本身導致的,其他類型不會出現這種變量中的成員引用自身的情況,所以垃圾回收只處理這兩種類型

object和array的回收過程

php7的垃圾回收包含兩個部分,一個是垃圾收回收期,一個是垃圾回收算法。

垃圾收集器,把可能是垃圾的元素收集到回收池中 也就是把變量的 zend_refcount>0的變量 放在一個buffer緩存區中。

垃圾回收的算法:等到回收池的值達到一定額度了,會進行統一遍歷把當前value標爲灰色(zend_refcounted_h.gc_info置爲GC_GREY),然後對當前value的成員進行深度深度優先遍歷,把所有成員變量value的refcount-1,進行模擬刪除,如果當前value的zend_refcount=0那就認爲是垃圾,直接刪除它(標記爲白色),如果不爲0,則排除了引用全部來自於自身成員的可能,表示還有外部的引用,並不是垃圾,然後需要還原,對所有成員進行深度遍歷,把成員refcount+1,同時標記爲黑色。

 然後再次遍歷buffer,將非GC_WHITE的節點從Buffer中刪除,最終buffer緩衝區全部爲真真的垃圾,然後將這些垃圾釋放,回收完成

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章