php垃圾回收機制,對於PHPer來說是一個不陌生但是又不是很熟悉的內容。那麼php是怎麼實現對不需要的記憶體進行回收的呢?
php變數的內部儲存結構
首先還是需要了解下基礎知識,便於垃圾回收原理內容的理解。大家都知道php是由C編寫而成的,所以php變數的內部儲存結構也會和C語言相關,即zval的結構體:
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; //引用計數記憶體中使用次數,為0刪除該變數
zend_uchar type; //變數型別
zend_uchar is_ref__gc; //區分是否是引用變數
};
複製程式碼
從上面結構體內容可以看出每一個php變數都會由變數型別
、value值
、引用計數次數
和是否是引用變數
四部分組成
注:上面zval結構體是php5.3版本之後的結構,php5.3之前因為沒有引入新的垃圾回收機制,即GC,所以命名也沒有_gc
;而php7版本之後由於效能問題所以改寫了zval結構,這裡不再表述
引用計數原理
瞭解了php變數的內部儲存結構之後,我們再瞭解下php變數賦值相關的原理和早期垃圾回收機制
變數容器
非array和object變數
每次將常量賦值給一個變數時,都會產生一個變數容器
舉例:
$a = '許錚的技術成長之路';
xdebug_debug_zval('a')
複製程式碼
結果:
a: (refcount=1, is_ref=0)='許錚的技術成長之路'
複製程式碼
array和object變數
會產生元素個數+1的變數容器
舉例:
$b = [
'name' => '許錚的技術成長之路',
'number' => 3
];
xdebug_debug_zval('b')
複製程式碼
結果:
b: (refcount=1, is_ref=0)=array ('name' => (refcount=1, is_ref=0)='許錚的技術成長之路', 'number' => (refcount=1, is_ref=0)=3)
複製程式碼
賦值原理(寫時複製技術)
瞭解了常量賦值之後,接下來我們從記憶體角度思考變數之間的賦值
舉例:
$a = [
'name' => '許錚的技術成長之路',
'number' => 3
]; //建立一個變數容器,變數a指向給變數容器,a的ref_count為1
$b = $a; //變數b也指向變數a指向的變數容器,a和b的ref_count為2
xdebug_debug_zval('a', 'b');
$b['name'] = '許錚的技術成長之路1';//變數b的其中一個元素髮生改變,此時會複製出一個新的變數容器,變數b重新指向新的變數容器,a和b的ref_count變成1
xdebug_debug_zval('a', 'b');
複製程式碼
結果:
a: (refcount=2, is_ref=0)=array ('name' => (refcount=1, is_ref=0)='許錚的技術成長之路', 'number' => (refcount=1, is_ref=0)=3)
b: (refcount=2, is_ref=0)=array ('name' => (refcount=1, is_ref=0)='許錚的技術成長之路', 'number' => (refcount=1, is_ref=0)=3)
a: (refcount=1, is_ref=0)=array ('name' => (refcount=1, is_ref=0)='許錚的技術成長之路', 'number' => (refcount=1, is_ref=0)=3)
b: (refcount=1, is_ref=0)=array ('name' => (refcount=1, is_ref=0)='許錚的技術成長之路1', 'number' => (refcount=1, is_ref=0)=3)
複製程式碼
所以,當變數a賦值給變數b的時候,並沒有立刻生成一個新的變數容器,而是將變數b指向了變數a指向的變數容器,即記憶體"共享";而當變數b其中一個元素髮生改變時,才會真正發生變數容器複製,這就是寫時複製技術
引用計數清0
當變數容器的ref_count計數清0時,表示該變數容器就會被銷燬,實現了記憶體回收,這也是php5.3版本之前的垃圾回收機制
舉例:
$a = "許錚的技術成長之路";
$b = $a;
xdebug_debug_zval('a');
unset($b);
xdebug_debug_zval('a');
複製程式碼
結果:
a: (refcount=2, is_ref=0)='許錚的技術成長之路'
a: (refcount=1, is_ref=0)='許錚的技術成長之路'
複製程式碼
迴圈引用引發的記憶體洩露問題
但是php5.3版本之前的垃圾回收機制存在一個漏洞,即當陣列或物件內部子元素引用其父元素,而此時如果發生了刪除其父元素的情況,此變數容器並不會被刪除,因為其子元素還在指向該變數容器,但是由於所有作用域內都沒有指向該變數容器的符號,所以無法被清除,因此會發生記憶體洩漏,直到該指令碼執行結束
舉例:
$a = array( 'one' );
$a[] = &$a;
xdebug_debug_zval( 'a' );
複製程式碼
由於該示例不好輸出結果,用圖表示,如圖:
舉例:
unset($a);
xdebug_debug_zval('a');
複製程式碼
如圖:
新的垃圾回收機制
php5.3版本之後引入根緩衝機制,即php啟動時預設設定指定zval數量的根緩衝區(預設是10000),當php發現有存在迴圈引用的zval時,就會把其投入到根緩衝區,當根緩衝區達到配置檔案中的指定數量(預設是10000)後,就會進行垃圾回收,以此解決迴圈引用導致的記憶體洩漏問題
確認為垃圾的準則
1、如果引用計數減少到零,所在變數容器將被清除(free),不屬於垃圾
2、如果一個zval 的引用計數減少後還大於0,那麼它會進入垃圾週期。其次,在一個垃圾週期中,通過檢查引用計數是否減1,並且檢查哪些變數容器的引用次數是零,來發現哪部分是垃圾。
總結
垃圾回收機制:
1、以php的引用計數機制為基礎(php5.3以前只有該機制)
2、同時使用根緩衝區機制,當php發現有存在迴圈引用的zval時,就會把其投入到根緩衝區,當根緩衝區達到配置檔案中的指定數量後,就會進行垃圾回收,以此解決迴圈引用導致的記憶體洩漏問題(php5.3開始引入該機制)