php垃圾回收機(jī)制,對(duì)于PHPer來說是一個(gè)不陌生但是又不是很熟悉的內(nèi)容。那么php是怎么實(shí)現(xiàn)對(duì)不需要的內(nèi)存進(jìn)行回收的呢?
php變量的內(nèi)存存儲(chǔ)結(jié)構(gòu):
首先還是需要了解下基礎(chǔ)知識(shí),便于垃圾回收原理內(nèi)容的理解。大家都知道php是由C編寫而成的,所以php變量的內(nèi)部存儲(chǔ)結(jié)構(gòu)也會(huì)和C語言相關(guān),即zval的結(jié)構(gòu)體:
struct _zval_struct { union { long lval; double dval; struct { char *val; int len; } str; HashTable *ht; zend_object_value obj; } value; //變量value值 zend_uint refcount__gc; //引用計(jì)數(shù)內(nèi)存中使用次數(shù),為0刪除該變量 zend_uchar type; //變量類型 zend_uchar is_ref__gc; //區(qū)分是否是引用變量 };
從上面結(jié)構(gòu)體內(nèi)容可以看出每一個(gè)php變量都會(huì)由變量類型、value值、引用計(jì)數(shù)次數(shù)和是否是引用變量四部分組成
注:上面zval結(jié)構(gòu)體是php5.3版本之后的結(jié)構(gòu),php5.3之前因?yàn)闆]有引入新的垃圾回收機(jī)制,即GC,所以命名也沒有_gc
;而php7版本之后由于性能問題所以改寫了zval結(jié)構(gòu),這里不再表述。
引用計(jì)數(shù)原理:
了解了php變量的內(nèi)部存儲(chǔ)結(jié)構(gòu)之后,我們?cè)倭私庀聀hp變量賦值相關(guān)的原理和早期垃圾回收機(jī)制
變量容器
非array和object變量
每次將常量賦值給一個(gè)變量時(shí),都會(huì)產(chǎn)生一個(gè)變量容器。
舉例:
$a = '許錚的技術(shù)成長之路'; xdebug_debug_zval('a')
結(jié)果:
a: (refcount=1, is_ref=0)='許錚的技術(shù)成長之路'
array和object變量
會(huì)產(chǎn)生元素個(gè)數(shù)+1的變量容器
舉例:
$b = [ 'name' => '許錚的技術(shù)成長之路', 'number' => 3 ]; xdebug_debug_zval('b')
結(jié)果:
b: (refcount=1, is_ref=0)=array ('name' => (refcount=1, is_ref=0)='許錚的技術(shù)成長之路', 'number' => (refcount=1, is_ref=0)=3)
賦值原理(寫時(shí)復(fù)制技術(shù))
了解了常量賦值之后,接下來我們從內(nèi)存角度思考變量之間的賦值
舉例:
$a = [ 'name' => '許錚的技術(shù)成長之路', 'number' => 3 ]; //創(chuàng)建一個(gè)變量容器,變量a指向給變量容器,a的ref_count為1 $b = $a; //變量b也指向變量a指向的變量容器,a和b的ref_count為2 xdebug_debug_zval('a', 'b'); $b['name'] = '許錚的技術(shù)成長之路1';//變量b的其中一個(gè)元素發(fā)生改變,此時(shí)會(huì)復(fù)制出一個(gè)新的變量容器, 變量b重新指向新的變量容器,a和b的ref_count變成1 xdebug_debug_zval('a', 'b');
結(jié)果:
a: (refcount=2, is_ref=0)=array ('name' => (refcount=1, is_ref=0)='許錚的技術(shù)成長之路', 'number' => (refcount=1, is_ref=0)=3) b: (refcount=2, is_ref=0)=array ('name' => (refcount=1, is_ref=0)='許錚的技術(shù)成長之路', 'number' => (refcount=1, is_ref=0)=3) a: (refcount=1, is_ref=0)=array ('name' => (refcount=1, is_ref=0)='許錚的技術(shù)成長之路', 'number' => (refcount=1, is_ref=0)=3) b: (refcount=1, is_ref=0)=array ('name' => (refcount=1, is_ref=0)='許錚的技術(shù)成長之路1', 'number' => (refcount=1, is_ref=0)=3)
所以,當(dāng)變量a賦值給變量b的時(shí)候,并沒有立刻生成一個(gè)新的變量容器,而是將變量b指向了變量a指向的變量容器,即內(nèi)存"共享";而當(dāng)變量b其中一個(gè)元素發(fā)生改變時(shí),才會(huì)真正發(fā)生變量容器復(fù)制,這就是寫時(shí)復(fù)制技術(shù)
引用計(jì)數(shù)清0
當(dāng)變量容器的ref_count計(jì)數(shù)清0時(shí),表示該變量容器就會(huì)被銷毀,實(shí)現(xiàn)了內(nèi)存回收,這也是php5.3版本之前的垃圾回收機(jī)制
舉例:
$a = "許錚的技術(shù)成長之路"; $b = $a; xdebug_debug_zval('a'); unset($b); xdebug_debug_zval('a');
結(jié)果:
a: (refcount=2, is_ref=0)='許錚的技術(shù)成長之路' a: (refcount=1, is_ref=0)='許錚的技術(shù)成長之路'
循環(huán)引用引發(fā)的內(nèi)存泄漏問題:
但是php5.3版本之前的垃圾回收機(jī)制存在一個(gè)漏洞,即當(dāng)數(shù)組或?qū)ο髢?nèi)部子元素引用其父元素,而此時(shí)如果發(fā)生了刪除其父元素的情況,此變量容器并不會(huì)被刪除,因?yàn)槠渥釉剡€在指向該變量容器,但是由于所有作用域內(nèi)都沒有指向該變量容器的符號(hào),所以無法被清除,因此會(huì)發(fā)生內(nèi)存泄漏,直到該腳本執(zhí)行結(jié)束
舉例:
$a = array( 'one' ); $a[] = &$a; xdebug_debug_zval( 'a' );
由于該示例不好輸出結(jié)果,用圖表示,如圖:
舉例:
unset($a); xdebug_debug_zval('a');
如圖:
新的垃圾回收機(jī)制:
php5.3版本之后引入根緩沖機(jī)制,即php啟動(dòng)時(shí)默認(rèn)設(shè)置指定zval數(shù)量的根緩沖區(qū)(默認(rèn)是10000),當(dāng)php發(fā)現(xiàn)有存在循環(huán)引用的zval時(shí),就會(huì)把其投入到根緩沖區(qū),當(dāng)根緩沖區(qū)達(dá)到配置文件中的指定數(shù)量(默認(rèn)是10000)后,就會(huì)進(jìn)行垃圾回收,以此解決循環(huán)引用導(dǎo)致的內(nèi)存泄漏問題
確認(rèn)為垃圾的準(zhǔn)則:
1、如果引用計(jì)數(shù)減少到零,所在變量容器將被清除(free),不屬于垃圾
2、如果一個(gè)zval 的引用計(jì)數(shù)減少后還大于0,那么它會(huì)進(jìn)入垃圾周期。其次,在一個(gè)垃圾周期中,通過檢查引用計(jì)數(shù)是否減1,并且檢查哪些變量容器的引用次數(shù)是零,來發(fā)現(xiàn)哪部分是垃圾。
總結(jié):
垃圾回收機(jī)制:
1、以php的引用計(jì)數(shù)機(jī)制為基礎(chǔ)(php5.3以前只有該機(jī)制)
2、同時(shí)使用根緩沖區(qū)機(jī)制,當(dāng)php發(fā)現(xiàn)有存在循環(huán)引用的zval時(shí),就會(huì)把其投入到根緩沖區(qū),當(dāng)根緩沖區(qū)達(dá)到配置文件中的指定數(shù)量后,就會(huì)進(jìn)行垃圾回收,以此解決循環(huán)引用導(dǎo)致的內(nèi)存泄漏問題(php5.3開始引入該機(jī)制)