1.php變量的實現
變量名 zval ,變量值 zend_value,php7的變量內存管理的引用計數 在zend_value結構上,變量的操作也都是zend_value實現的。
//zend_types.h typedef struct _zval_struct zval; typedef union _zend_value { zend_long lval; //int整形 double dval; //浮點型 zend_refcounted *counted; zend_string *str; //string字符串 zend_array *arr; //array數組 zend_object *obj; //object對象 zend_resource *res; //resource資源類型 zend_reference *ref; //引用類型,通過&$var_name定義的 zend_ast_ref *ast; //下面幾個都是內核使用的value zval *zv; void *ptr; zend_class_entry *ce; zend_function *func; struct { uint32_t w1; uint32_t w2; } ww; } zend_value; struct _zval_struct { zend_value value; //變量實際的value union { struct { ZEND_ENDIAN_LOHI_4( //這個是為了兼容大小字節序,小字節序就是下面的順序,大字節序則下面4個順序翻轉 zend_uchar type, //變量類型 zend_uchar type_flags, //類型掩碼,不同的類型會有不同的幾種屬性,內存管理會用到 zend_uchar const_flags, zend_uchar reserved) //call info,zend執行流程會用到 } v; uint32_t type_info; //上面4個值的組合值,可以直接根據type_info取到4個對應位置的值 } u1; union { uint32_t var_flags; uint32_t next; //哈希表中解決哈希沖突時用到 uint32_t cache_slot; /* literal cache slot */ uint32_t lineno; /* line number (for ast nodes) */ uint32_t num_args; /* arguments number for EX(This) */ uint32_t fe_pos; /* foreach position */ uint32_t fe_iter_idx; /* foreach iterator index */ } u2; //一些輔助值 };
zval
結構比較簡單,內嵌一個union類型的zend_value
保存具體變量類型的值或指針,zval
中還有兩個union:u1
、u2
:
u1: 它的意義比較直觀,變量的類型就通過u1.v.type
區分,另外一個值type_flags
為類型掩碼,在變量的內存管理、gc機制中會用到,第三部分會詳細分析,至於后面兩個const_flags
、reserved
暫且不管
u2: 這個值純粹是個輔助值,假如zval
只有:value
、u1
兩個值,整個zval的大小也會對齊到16byte,既然不管有沒有u2大小都是16byte,把多余的4byte拿出來用於一些特殊用途還是很划算的,比如next在哈希表解決哈希沖突時會用到,還有fe_pos在foreach會用到......
從zend_value
可以看出,除long
、double
類型直接存儲值外,其它類型都為指針,指向各自的結構。
zval.u1.type
類型
/* regular data types */ #define IS_UNDEF 0 #define IS_NULL 1 #define IS_FALSE 2 #define IS_TRUE 3 #define IS_LONG 4 #define IS_DOUBLE 5 #define IS_STRING 6 #define IS_ARRAY 7 #define IS_OBJECT 8 #define IS_RESOURCE 9 #define IS_REFERENCE 10 /* constant expressions */ #define IS_CONSTANT 11 #define IS_CONSTANT_AST 12 /* fake types */ #define _IS_BOOL 13 #define IS_CALLABLE 14 /* internal types */ #define IS_INDIRECT 15 #define IS_PTR 17
簡單的類型是true、false、long、double、null,其中true、false、null沒有value,直接根據type區分,而long、double的值則直接存在value中
字符串結構
struct _zend_string { zend_refcounted_h gc; zend_ulong h; /* hash value */ size_t len; char val[1]; };
- gc: 變量引用信息,比如當前value的引用數,所有用到引用計數的變量類型都會有這個結構,3.1節會詳細分析
- h: 哈希值,數組中計算索引時會用到
- len: 字符串長度,通過這個值保證二進制安全
- val: 字符串內容,分配時按len長度申請內存
字符串分類:
IS_STR_PERSISTENT(通過malloc分配的)
IS_STR_INTERNED(php代碼里寫的一些字面量,比如函數名、變量值)
IS_STR_PERMANENT(永久值,生命周期大於request)
IS_STR_CONSTANT(常量)
IS_STR_CONSTANT_UNQUALIFIED
數組結構 php數組的底層結構是hashTabe (php內核的函數 類 文件索引表 全局符號表頁都是用hash結構實現的)
hash結構根據hash碼進行訪問的key-value數據結構,根據key映射函數直接索引到value,采用直接尋址直接映射key到內存地址,查找的期望時間O(1)
typedef struct _zend_array HashTable; struct _zend_array { zend_refcounted_h gc; //引用計數信息,與字符串相同 union { struct { ZEND_ENDIAN_LOHI_4( zend_uchar flags, zend_uchar nApplyCount, zend_uchar nIteratorsCount, zend_uchar reserve) } v; uint32_t flags; } u; uint32_t nTableMask; //計算bucket索引時的掩碼 Bucket *arData; //bucket數組 uint32_t nNumUsed; //已用bucket數 uint32_t nNumOfElements; //已有元素數,nNumOfElements <= nNumUsed,因為刪除的並不是直接從arData中移除 uint32_t nTableSize; //數組的大小,為2^n uint32_t nInternalPointer; //數值索引 zend_long nNextFreeElement; dtor_func_t pDestructor; };
arData
指向存儲元素數組的第一個Bucket,插入元素時按順序 依次插入 數組,比如第一個元素在arData[0]、第二個在arData[1]...arData[nNumUsed]。PHP數組的有序性正是通過arData
保證的,arData並不是按key映射的散列表,列表在ht->arData內存之前,分配內存時這個散列表與Bucket數組一起分配,arData向后移動到了Bucket數組的起始位置,並不是申請內存的起始位置,這樣散列表可以由arData指針向前移動訪問到,即arData[-1]、arData[-2]、arData[-3]......散列表的結構是uint32_t
,它保存的是value在Bucket數組中的位置。
hash碰撞 不同的key可能計算得到相同的哈希值(數值索引的哈希值直接就是數值本身),但是這些值又需要插入同一個散列表。一般解決方法是將Bucket串成鏈表,查找時遍歷鏈表比較key。
數組擴容 容量不夠時檢查 刪除元素的比例,達到一個閾值時重建索引,沒有達到時直接擴容為當前大小的兩倍,復制bucket到新的內存空間重建索引
重建散列表 當刪除元素達到一定數量或擴容后都需要重建散列表,因為value在Bucket位置移動了或哈希數組nTableSize變化了導致key與value的映射關系改變,重建過程實際就是遍歷Bucket數組中的value,然后重新計算映射值更新到散列表。
hash元素刪除 一個元素從哈希表刪除時並不會將對應的Bucket移除,而是將Bucket存儲的zval修改為IS_UNDEF
,只有擴容時發現nNumOfElements與nNumUsed相差達到一定數量(這個數量是:ht->nNumUsed - ht->nNumOfElements > (ht->nNumOfElements >> 5)
)時才會將已刪除的元素全部移除,重新構建哈希表 。
對象/資源結構
struct _zend_object { zend_refcounted_h gc; uint32_t handle; zend_class_entry *ce; //對象對應的class類 const zend_object_handlers *handlers; HashTable *properties; //對象屬性哈希表 zval properties_table[1]; }; struct _zend_resource { zend_refcounted_h gc; int handle; int type; void *ptr; };
對象比較常見,資源指的是tcp連接、文件句柄等等類型,這種類型比較靈活,可以隨意定義struct,通過ptr指向。
引用類型結構
struct _zend_reference { zend_refcounted_h gc; zval val; };
引用是PHP中比較特殊的一種類型,它實際是指向另外一個PHP變量,對它的修改會直接改動實際指向的zval,可以簡單的理解為C中的指針,但是PHP中的 引用只可能有一層 ,不會出現一個引用指向另外一個引用的情況 ,也就是沒有C語言中指針的指針
的概念。
2.內存管理
引用計數 使用的是refcount字段,變量復制和函數傳參的時候直接refcount++,銷毀變量refcount--,refcount=0銷毀變量。
簡單的標量數據類型不會用到引用計數直接是硬拷貝,只有value是指針的數據類型使用引用計數。
字符串的內部字符串類型不會用到引用計數,函數名 類名 靜態字符串 變量名等字面量都是這種數據類型
寫時復制
引用計數之后,如果其中一個變量試圖更改value的內容則會重新拷貝一份value修改,同時斷開舊的指向。
變量回收 主動銷毀(unset)、自動銷毀()
PHP變量的回收是根據refcount實現的,當unset、return時會將變量的引用計數減掉,如果refcount減到0則直接釋放value,這是變量的簡單gc過程,但是實際過程中array、object兩種類型中出現gc無法回收導致內存泄漏的bug。
當銷毀一個變量時,如果發現減掉refcount后仍然大於0,且類型是IS_ARRAY、IS_OBJECT則將此value放入gc可能垃圾雙向鏈表(緩沖buffer)中,等這個鏈表達到一定數量(10000個值)后啟動檢查程序將所有變量檢查一遍,如果確定是垃圾則銷毀釋放。
遍歷 buffer鏈表,把value標記 灰色-》 深度遍歷value成員 refcount-- 標記灰色 -》 再次遍歷buffer鏈表檢查value的refcount是否是0 -》 是0表示是垃圾標記為白色,不是0表示不是垃圾 -》深度遍歷不是0的value把所有成員變量的refcount++標記為黑色 -〉遍歷buffer把白色的幾點刪除 清除這些垃圾