小而巧的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中 除了 array
和object
類型的變量,其餘大部分是自動回收,在自動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緩衝區全部爲真真的垃圾,然後將這些垃圾釋放,回收完成