引用鏈接:php變量與gc
棧區(qū)stack
存儲(chǔ)參數(shù)值,局部變量杂抽,維護(hù)函數(shù)調(diào)用關(guān)系
堆區(qū)heap
動(dòng)態(tài)內(nèi)存區(qū)域自点,隨時(shí)申請(qǐng)和釋放遂赠,需要自行處理生存周期
全局區(qū)(靜態(tài)區(qū))
存儲(chǔ)全局和靜態(tài)變量
字面量區(qū)
常量字符串存儲(chǔ)區(qū)
程序代碼區(qū)
存儲(chǔ)二進(jìn)制代碼
PHP5的zval
zval核心是一個(gè)zvalue_value的union和zend_uchar類型type組成久妆,5.3以后引入refcount__gc字段通過引用計(jì)數(shù)進(jìn)行垃圾回收,同時(shí)新增了新的is_ref_gc來標(biāo)記是否是引用類型
_zval_value只有5個(gè)字段跷睦,但是PHP有8種數(shù)據(jù)結(jié)構(gòu)筷弦,布爾型、整型和資源型都是lval字段存儲(chǔ)的抑诸,dval存浮點(diǎn)烂琴,str存字符串,ht存數(shù)組蜕乡,obj存對(duì)象奸绷,如果所有字段都是0就是null
_zvalue_value的lval、dval為8字節(jié)层玲,str12字節(jié),obj為12字節(jié)线椰,因?yàn)閮?nèi)存對(duì)齊卿捎,需要兩個(gè)8字節(jié)來存趟庄,所以一共為16字節(jié)
_zval_struct的refcount__gc為4猫十、type和is_ref_gc都是1,所以需要24字節(jié)
申請(qǐng)一個(gè)變量是(zval*)emalloc(sizeof(zval_gc_info))
zval_gc_info是一個(gè)zval結(jié)構(gòu)體和一個(gè)u聯(lián)合體組成汇荐,u為8字節(jié)
實(shí)際申請(qǐng)一個(gè)變量是32字節(jié)
因?yàn)檎魏透↑c(diǎn)型不需要進(jìn)行g(shù)c革娄,所以會(huì)有內(nèi)存浪費(fèi)
在開啟zend內(nèi)存池,zval_gc_info在內(nèi)存池中分配,內(nèi)存池會(huì)為每個(gè)zval_gc_info額外申請(qǐng)一個(gè)大小為16字節(jié)的zend_mm_block結(jié)構(gòu)體
最終一個(gè)變量實(shí)際占用48字節(jié)
PHP5的問題
1.PHP5最大的問題就是zend_object_value需要12個(gè)字節(jié)俱恶,這個(gè)應(yīng)該很容易優(yōu)化掉,如把它移動(dòng)出來用一個(gè)指針代替
2.zend_struct每一個(gè)字段都有明確的含義,沒有預(yù)留任何自定義字段,導(dǎo)致做優(yōu)化時(shí)候需要存儲(chǔ)一些zval相關(guān)信息時(shí)候不得不采用其他結(jié)構(gòu)體映射窜管,或者外部包裝打補(bǔ)丁方式來擴(kuò)容zval如zval_gc_info,就是擴(kuò)容了u的union碱茁,比如GC只關(guān)心IS_ARRAY和IS_OBJECT類型但是要用到32個(gè)字節(jié)
3.PHP的zval大部分都是按值傳遞, 寫時(shí)拷貝的值, 但是有倆個(gè)例外, 就是對(duì)象和資源, 他們永遠(yuǎn)都是按引用傳遞, 這樣就造成一個(gè)問題, 對(duì)象和資源在除了zval中的引用計(jì)數(shù)以外, 還需要一個(gè)全局的引用計(jì)數(shù), 這樣才能保證內(nèi)存可以回收. 所以在PHP5的時(shí)代, 以對(duì)象為例, 它有倆套引用計(jì)數(shù), 一個(gè)是zval中的, 另外一個(gè)是obj自身的計(jì)數(shù)
獲取object為EG(objects_store).object_buckets[Z_OBJ_HANDLE_P(z)].bucket.obj纽竣,要經(jīng)過多次內(nèi)存讀取
4.PHP中大量的計(jì)算都是面向字符串的, 然而因?yàn)橐糜?jì)數(shù)是作用在zval的, 那么就會(huì)導(dǎo)致如果要拷貝一個(gè)字符串類型的zval, 我們別無他法只能復(fù)制這個(gè)字符串. 當(dāng)我們把一個(gè)zval的字符串作為key添加到一個(gè)數(shù)組里的時(shí)候, 我們別無他法只能復(fù)制這個(gè)字符串穴吹;PHP中大量的結(jié)構(gòu)體都是基于Hashtable實(shí)現(xiàn)的, 增刪改查Hashtable的操作占據(jù)了大量的CPU時(shí)間, 而字符串要查找首先要求它的Hash值, 理論上我們完全可以把一個(gè)字符串的Hash值計(jì)算好以后, 就存下來, 避免再次計(jì)算等等
php7的zval
在_zval_struct中硝逢,還有兩個(gè)重要字段u1叫乌、u2
v和type_info公用一塊內(nèi)存柴罐,長(zhǎng)度均是4,修改type_info等同于修改v中的值憨奸,type_info就是v中的4個(gè)char的組合
type | 記錄變量的類型 PHP7中的字段類型使用u1.v.type來表示 |
type_flags | 變量類型特有的標(biāo)記革屠,不同類型的變量對(duì)應(yīng)的flag也不同 常量類型、不可變類型排宰、需要引用計(jì)數(shù)的類型似芝、可能包含循環(huán)引用的類型、可被復(fù)制的類型 |
const_flags | 常量類型的標(biāo)記 |
reserved | 保留字段 如字符串板甘,u1.v.type值為6(IS_STRING)党瓮,字符串又是可以引用和可以拷貝的,所以u(píng)1.v.type_flag值為24(S_TYPE_COPYABLE|IS_TYPE_REFCOUNTED)盐类,這樣u1.type_info為6150 |
IS_UNDEF為標(biāo)記為未定義寞奸,表示數(shù)據(jù)可以被覆蓋或者刪除,如Unset操作在跳,PHP7不會(huì)直接將數(shù)據(jù)從分配給hashtable的內(nèi)存中刪除枪萄,而是先將該元素所在的bucket位置標(biāo)記為IS_UNDEF,當(dāng)hashtable中的IS_UNDEF元素個(gè)數(shù)達(dá)到一定閾值時(shí)候猫妙,進(jìn)行rehash操作時(shí)候再將元素覆蓋或者刪除
整型和浮點(diǎn)型的值拷貝
$a = 10
$a = zval_1(u1.v.type=IS_LONG,value.lval=10)
$b = $a
$a = zval_1(u1.v.type=IS_LONG,value.lval=10)
$b = zval_2(u1.v.type=IS_LONG,value.lval=10)
因?yàn)閦val只有16字節(jié)瓷翻,所以沒有直接做寫時(shí)拷貝,而是直接做了拷貝
$a = 20
$a = zval_1(u1.v.type=IS_LONG,value.lval=20)
$b = zval_2(u1.v.type=IS_LONG,value.lval=10)
unset($a)
$a = zval_1(u1.v.type=IS_UNDEF,value.lval=20)
$b = zval_2(u1.v.type=IS_LONG,value.lval=10)
字符串
冗余了hash值h吐咳,避免了在數(shù)組操作中hash值的重復(fù)計(jì)算
len表示字符串的長(zhǎng)度
val記錄了字符串的內(nèi)容
字符串通過zval.str指向zend_string結(jié)構(gòu)體
引用
PHP5在采用了引用計(jì)數(shù)后逻悠,使用refcount__gc來記錄次數(shù),同時(shí)使用is_ref_gc來記錄是否是引用類型
$a = 'hello'
$a -> zval1(type=IS_STRING,refcount_gc=1,is_ref_gc=0)
$b = $a
$b,$a -> zval1(type=IS_STRING,refcount_gc=2,is_ref_gc=0)
$c = &$b
$a -> zval1(type=IS_STRING,refcount_gc=1,is_ref_gc=0)
$c,$b -> zval2(type=IS_STRING,refcount_gc=2,is_ref_gc=1)
PHP7的zval沒有存儲(chǔ)引用計(jì)數(shù)相關(guān)的信息韭脊,引入了新的類型IS_REFERENCE來處理&
$a = 'hello'.time()
$a -> zend_string(refcount=1,val)
$b = $a
$b,$a -> zend_string(refcount=2,val)
$c = &$b
$a -> zend_string(refcount=2,val)
$b,$c ->zval(type=IS_REFRENCE,refcount=2)->zend_string(refcount=2,val)
當(dāng)使用&操作符時(shí)候童谒,會(huì)創(chuàng)建一種新的中繼結(jié)構(gòu)體zend_reference,這個(gè)結(jié)構(gòu)體會(huì)指向真正的zend_string結(jié)構(gòu)體
PHP5的情況如下
PHP7的情況如下
對(duì)于zval在value字段中就能保存下來的沪羔,就不會(huì)對(duì)他們進(jìn)行引用計(jì)數(shù)
$a = 123
$b = $a //此時(shí)refcount和is_ref都是0
$c = &$a; //此時(shí)a和c的refcount是2饥伊,is_ref是1,b的refcount和is_ref都是0
因?yàn)?amp;操作會(huì)申請(qǐng)一個(gè)新的zend_reference結(jié)構(gòu)蔫饰,將zend_reference指向原來的zval_struct.value
不可變數(shù)組琅豆,偽引用計(jì)數(shù)為2
PHP7的改進(jìn)優(yōu)化
1.zval只需要16字節(jié),保留了擴(kuò)充字段u1、u2兩個(gè)union篓吁,u2為輔助字段茫因,如u2的next用來取代hashtable中原來的拉鏈法指針
2.PHP5時(shí)候的IS_BOOL拆分成了IS_FALSE和IS_TRUE,類型檢查的時(shí)候更快
3.IS_LONG和IS_DOUBLE在拷貝的時(shí)候直接賦值杖剪,省去了大量的引用計(jì)數(shù)操作冻押,如$a = 1驰贷,refcount為0;對(duì)于只有類型而沒有值的也不需要引用計(jì)數(shù)了IS_NULL洛巢、IS_FALSE括袒、IS_TRUE
4.對(duì)于復(fù)雜類型,一個(gè)value保存不下來的稿茉,就用value來保存一個(gè)指針, 這個(gè)指針指向這個(gè)具體的值, 引用計(jì)數(shù)也隨之作用于這個(gè)值上, 而不在是作用于zval上了
以IS_ARRY為例子
zval.value.arr將指向上面的這樣的一個(gè)結(jié)構(gòu)體, 由它實(shí)際保存一個(gè)數(shù)組, 引用計(jì)數(shù)部分保存在zend_refcounted_h結(jié)構(gòu)中
所有的復(fù)雜類型的定義, 開始的時(shí)候都是zend_refcounted_h結(jié)構(gòu), 這個(gè)結(jié)構(gòu)里除了引用計(jì)數(shù)以外, 還有GC相關(guān)的結(jié)構(gòu). 從而在做GC回收的時(shí)候, GC不需要關(guān)心具體類型是什么, 所有的它都可以當(dāng)做zend_refcounted*結(jié)構(gòu)來處理
對(duì)象
PHP5的對(duì)象存儲(chǔ)在_zend_object_value結(jié)構(gòu)體中
handle是一個(gè)無符號(hào)的int锹锰,通過handler可以在全局的對(duì)象池里面索引到指定對(duì)象;handlers指向了一個(gè)包含多個(gè)函數(shù)指針的結(jié)構(gòu)體漓库,但是對(duì)象的真正數(shù)據(jù)并沒有在這里恃慧,而是存在全局的EG(objects_store)中
對(duì)象在zend_object_store_bucket中維護(hù)了另外一個(gè)refcount來記錄對(duì)象的引用計(jì)數(shù)
PHP5對(duì)象的問題是兩套引用計(jì)數(shù)和獲取對(duì)象的多次內(nèi)存讀取,使得對(duì)象效率比較低
PHP7中對(duì)象
PHP7對(duì)象的屬性存儲(chǔ)在properties_table數(shù)組中米苹,而properties是一個(gè)hashtable糕伐,key為對(duì)象屬性的名字,value為屬性值在properties_table數(shù)組中的偏移量蘸嘶,通過偏移量去查找真正的值
PHP5和7的change on write
$a = "123".time()
a;
a;
在PHP5中良瞧,a是引用關(guān)系,refcount=2训唱,is_ref_gc為1褥蚯,當(dāng)
a時(shí)候,發(fā)現(xiàn)
a的復(fù)制
在PHP7中况增,a首先生成一個(gè)refernce類型赞庶,然后因?yàn)榇藭r(shí)有倆個(gè)變量引用它所以zend_reference這個(gè)結(jié)構(gòu)的引用計(jì)數(shù)zval.value.ref->gc.refcount為2,然后
a時(shí)候澳骤,讓$c指向zval.value.ref->val就可以了歧强,沒有產(chǎn)生復(fù)制
gc基本
PHP7中復(fù)雜類型的引用計(jì)數(shù)都維護(hù)在各個(gè)結(jié)構(gòu)體頭部的gc中
gc是一種內(nèi)存管理機(jī)制,但一個(gè)變量不需要時(shí)候應(yīng)該被釋放为肮,一種方式是使用引用計(jì)數(shù)摊册,通過對(duì)數(shù)據(jù)存儲(chǔ)的物理空間多附加一個(gè)計(jì)數(shù)器,當(dāng)其他數(shù)據(jù)與其相關(guān)就自增颊艳,定期檢查存儲(chǔ)對(duì)象的計(jì)數(shù)器
PHP7的gc實(shí)現(xiàn)方式是定期遍歷和標(biāo)記若干存儲(chǔ)對(duì)象的數(shù)組茅特,再通過算法將是垃圾的物理空間回收
總大小為8字節(jié)
zend_uchar type冗余了一份u1.v.type,1字節(jié)
zend_uchar flags可以是字符串類型或者是數(shù)組類型等棋枕,1字節(jié)
gc_info 為2字節(jié)白修,標(biāo)記當(dāng)前元素的顏色和位置,黑色重斑、白色兵睛、灰色、紫色
如
$i = 0;
$a = "hello" . $i;
$b = $a;
a并沒有進(jìn)行內(nèi)存拷貝,而是直接指向了同一個(gè)zend_string結(jié)構(gòu)體
unset($a)
refcount為1
unset($b)
refcount為0祖很,調(diào)用ZVAL_UNDEF(var)累盗,將type標(biāo)記為IS_UNDEF
循環(huán)引用問題
PHP7中使用&會(huì)改變等號(hào)兩邊zval的類型為IS_REFERENCE,引用計(jì)數(shù)會(huì)在新的結(jié)構(gòu)體zend_reference中突琳,并且引用計(jì)數(shù)為2
如果等號(hào)兩邊是同一個(gè)變量,那么就自己引用自己
$a = []
$a[] = &$a;
![image](https://upload-images.jianshu.io/upload_images/6578832-358334a17824748e?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
當(dāng)執(zhí)行unset($a)操作符相,$a所在的zval類型被標(biāo)記為IS_UNDEF拆融,zend_reference結(jié)構(gòu)體的引用計(jì)數(shù)減1,但是仍然大于0啊终,后面的結(jié)構(gòu)體可能變成垃圾镜豹,如果循環(huán)引用不處理可能會(huì)造成內(nèi)存泄漏,gc會(huì)將這部分可能是垃圾的數(shù)據(jù)收集到緩沖區(qū)蓝牲,同時(shí)加入root環(huán)
$a = []
$a[] = &$a;
unset($a)
a的第一個(gè)元素指向a趟脂,a的zval的refcount為2,unset($a)例衍, refcount為1昔期,但是不會(huì)被回收
![image](https://upload-images.jianshu.io/upload_images/6578832-9cd0a47cf5766fc5?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
PHP是通過符號(hào)表(Symbol Table)存儲(chǔ)變量符號(hào)的,全局有一個(gè)符號(hào)表佛玄,而每個(gè)復(fù)雜類型如數(shù)組或?qū)ο笥凶约旱姆?hào)表硼一,因此上面代碼中,a和a[0]是兩個(gè)符號(hào)梦抢,但是a儲(chǔ)存在全局符號(hào)表中般贼,而a[0]儲(chǔ)存在數(shù)組本身的符號(hào)表中,且這里a和a[0]引用同一個(gè)zval(當(dāng)然符號(hào)a后來被銷毀了)
##### gc
PHP7的gc為垃圾收集器將可能是垃圾的元素收集在回收池中奥吩,然后由垃圾回收算法回收
1.如果一個(gè)變量value的refcount減少到0哼蛆,此value可以被釋放,不屬于垃圾(gc不會(huì)處理)
2.如果一個(gè)變量value的refcount減少后大于0霞赫,此value不可以被釋放腮介,屬于垃圾
目前垃圾是會(huì)出現(xiàn)在array和object中,object是成員屬性引用對(duì)象本身
unset($a)后绩脆,對(duì)a進(jìn)行析構(gòu)函數(shù)將refcount-1萤厅,若為0則說明可以直接釋放內(nèi)存,若大于0則放到gc_root_buffer中靴迫,每個(gè)zval只可放一次惕味,依據(jù)是zval所在的zval_gc_info中g(shù)c_root_buffer的顏色是否為紫色
在自動(dòng)GC中,在zval斷開value的指向時(shí)如果發(fā)現(xiàn)refcount=0會(huì)直接釋放玉锌,發(fā)生斷開的常見的為修改變量與函數(shù)返回名挥,函數(shù)返回會(huì)釋放所有的局部變量,把所有的局部變量的refcount-1
如果zval的refcount減少到0主守,那么zval可以被釋放禀倔,不屬于垃圾
如果zval的refcount減少后大于0榄融,那么zval還不能被釋放,zval可能是一個(gè)垃圾救湖,放入緩存區(qū)
如下代碼會(huì)造成內(nèi)存溢出愧杯,因?yàn)殛P(guān)閉了gc導(dǎo)致res不能被回收
![](https://upload-images.jianshu.io/upload_images/6578832-b29c07820f551391?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
![](https://upload-images.jianshu.io/upload_images/6578832-543e2208f19c525c?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
gc_root_buffer是一個(gè)雙向鏈表,同時(shí)記錄引用計(jì)數(shù)相關(guān)信息鞋既,zend_gc_globals維護(hù)著gc的整個(gè)信息
| | |
| ------ | ------ |
|gc_enabled | 是否開啟gc|
|gc_active | 垃圾回收算法是否運(yùn)行|
|gc_full | 垃圾緩沖區(qū)是否滿了|
|buf | 垃圾緩沖區(qū)力九,默認(rèn)大小為10000個(gè)節(jié)點(diǎn),第0個(gè)節(jié)點(diǎn)保留邑闺,不會(huì)使用(#define GC_ROOT_BUFFER_MAX_ENTRIES 10001)
|roots | 指向緩沖區(qū)中最新加入的可能是垃圾的元素|
|unused | 指向緩沖區(qū)中沒有使用的位置跌前,在沒有啟動(dòng)gc算法前,指向空|
|first_unused | 指向緩沖區(qū)中第一個(gè)未使用的位置陡舅,新的元素插入緩沖區(qū)后抵乓,指針向后移動(dòng)一位|
|last_unused | 指向緩沖區(qū)最后一個(gè)位置|
| to_free | 待釋放的列表|
| next_to_free | 下一個(gè)待釋放的列表|
| gc_runs | 記錄gc算法運(yùn)行的次數(shù),當(dāng)緩沖區(qū)滿了靶衍,才會(huì)運(yùn)行g(shù)c|
| collected | 記錄gc算法回收的垃圾數(shù)|
zend_gc_globals大小為120字節(jié)灾炭,PHP7維護(hù)了一個(gè)全局變量zend_gc_globals的hashtable
![image](https://upload-images.jianshu.io/upload_images/6578832-20edac0d2281fb43?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
存取值的宏為GC_G(v)
gc的初始化
![](https://upload-images.jianshu.io/upload_images/6578832-4ce076de1e4cac57?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
![](https://upload-images.jianshu.io/upload_images/6578832-3cfefdf218ec426b?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
###### 垃圾回收過程
1.類型是數(shù)組和對(duì)象
2.沒有在緩存區(qū)存在
3.沒有被標(biāo)記過
4.將其gc_info標(biāo)記為紫色,且記錄其在緩沖區(qū)的位置
當(dāng)緩沖區(qū)滿了摊灭,在收集到新的元素就會(huì)觸發(fā)gc算法咆贬,引用計(jì)數(shù)大于0說明還有其他地方使用,那么先將引用計(jì)數(shù)-1帚呼,如果為0則說明是垃圾掏缎,需要被回收。反之說明不是垃圾煤杀,需要將其從回收池移除
###### gc算法過程
1.對(duì)roots環(huán)中的每個(gè)元素進(jìn)行深度優(yōu)先遍歷眷蜈,將每個(gè)元素中的gc_info為紫色的標(biāo)記為灰色,且引用計(jì)數(shù)-1
2.掃描roots環(huán)中g(shù)c_info為灰色的元素沈自,如果發(fā)現(xiàn)引用計(jì)數(shù)仍然大于0酌儒,說明不為垃圾,那么將其顏色重新標(biāo)記為黑色枯途,并且引用計(jì)數(shù)+1,忌怎;如果發(fā)現(xiàn)引用計(jì)數(shù)為0,標(biāo)記為白色
3.掃描roots環(huán)酪夷,將gc_info為黑色的從roots中移除榴啸,然后的白色的元素進(jìn)行深度優(yōu)先遍歷,將其引用計(jì)數(shù)+1晚岭,然后將roots鏈表移動(dòng)到待釋放的列表to_free中
4.釋放to_free列表的元素
![image](https://upload-images.jianshu.io/upload_images/6578832-aea7083b35c4f577?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
對(duì)象是類的實(shí)例鸥印,有繼承類的默認(rèn)屬性表default_properties_table,同時(shí)類支持動(dòng)態(tài)屬性,所以也有自己的properties_table库说,在對(duì)類進(jìn)行深度優(yōu)先遍歷時(shí)候會(huì)將兩個(gè)表重建合并
###### gc配置
* php.ini
![php.ini](https://upload-images.jianshu.io/upload_images/6578832-498bf06c331cc321?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
* gc_enable()
* gc_disable()
* gc_collect_cycles() //強(qiáng)制進(jìn)行垃圾回收
關(guān)閉了gc時(shí)候狂鞋,還會(huì)記錄到root根緩沖區(qū),但是當(dāng)根緩沖區(qū)滿了不會(huì)自動(dòng)進(jìn)行垃圾回收