php的垃圾回收機制

轉載請附上本文地址:http://blog.csdn.net/u011957758/article/details/76864400php

前言

是的,平時常常聽到大牛說到的gc,就是垃圾回收器,全稱Garbage Collection。算法

早期版本,準確地說是5.3以前(不包括5.3)的垃圾回收機制,是沒有專門的垃圾回收器的。只是簡單的判斷了一下變量的zval的refcount是否爲0,是的話就釋放不然不釋放直至進程結束。數組

乍一看確實沒毛病啊,然而其中隱藏着變量內存溢出的風險:http://bugs.php.net/bug.php?id=33595 ,沒法回收的內存形成了內存泄漏,因此PHP5.3出現了專門負責清理垃圾數據、防止內存泄漏的GC。bash

下文將由淺入深(憑感受)來記錄下php的垃圾回收機制是怎麼一回事?數據結構

1.php引用計數基本知識點函數

2.php的內存管理機制性能

3.php中垃圾是如何定義的?優化

4.老版本php中如何產生內存泄漏?ui

5.5.3版本之後php是如何處理垃圾內存的?atom

6.涉及到垃圾回收的知識點

php引用計數基本知識點

首先必需要先講講這個會引發垃圾回收的關鍵基數是怎麼回事?

關於php的zval結構體,以及refcount與is_ref的知識點,在菜鳥學php擴展 之 詳解php擴展的變量(四) 已描述很是清楚。

不許確但卻通俗的說: 
refcount:多少個變量是同樣的用了相同的值,這個數值就是多少。 
is_ref:bool類型,當refcount大於2的時候,其中一個變量用了地址&的形式進行賦值,好了,它就變成1了。

主要講講如何用php來直觀的看到這些計數的變化,走一波。 
首先須要在php上裝上xdebug的擴展。

1.第一步:查看內部結構

<?php $name = "咖啡色的羊駝"; xdebug_debug_zval('name');
  • 1
  • 2
  • 3

會獲得:

name:(refcount=1, is_ref=0),string '咖啡色的羊駝' (length=18)
  • 1

2.第二步:增長一個計數

<?php $name = "咖啡色的羊駝"; $temp_name = $name; xdebug_debug_zval('name');
  • 1
  • 2
  • 3
  • 4

會獲得:

name:(refcount=2, is_ref=0),string '咖啡色的羊駝' (length=18)
  • 1

看到了吧,refcount+1了。

3.第三步:引用賦值

<?php $name = "咖啡色的羊駝"; $temp_name = &$name; xdebug_debug_zval('name');
  • 1
  • 2
  • 3
  • 4

會獲得:

name:(refcount=2, is_ref=1),string '咖啡色的羊駝' (length=18)
  • 1

是的引用賦值會致使zval經過is_ref來標記是否存在引用的狀況。

4.第四步:數組型的變量

<?php $name = ['a'=>'咖啡色', 'b'=>'的羊駝']; xdebug_debug_zval('name');
  • 1
  • 2
  • 3

會獲得:

name:
(refcount=1, is_ref=0), array (size=2) 'a' => (refcount=1, is_ref=0),string '咖啡色' (length=9) 'b' => (refcount=1, is_ref=0),string '的羊駝' (length=9) 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

還挺好理解的,對於數組來看是一個總體,對於內部kv來看又是分別獨立的總體,各自都維護着一套zval的refount和is_ref。

5.第五步:銷燬變量

<?php $name = "咖啡色的羊駝"; $temp_name = $name; xdebug_debug_zval('name'); unset($temp_name); xdebug_debug_zval('name');
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

會獲得:

name:(refcount=2, is_ref=0),string '咖啡色的羊駝' (length=18) name:(refcount=1, is_ref=0),string '咖啡色的羊駝' (length=18)
  • 1
  • 2

refcount計數減1,說明unset並不是必定會釋放內存,當有兩個變量指向的時候,並不是會釋放變量佔用的內存,只是refcount減1.

php的內存管理機制

知道了zval是怎麼一回事,接下來看看如何經過php直觀看到內存管理的機制是怎麼樣的。

外在的內存變化

先來一段代碼:

<?php //獲取內存方法,加上true返回實際內存,不加則返回表現內存 var_dump(memory_get_usage()); $name = "咖啡色的羊駝"; var_dump(memory_get_usage()); unset($name); var_dump(memory_get_usage());
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

會獲得:

int 1593248 int 1593384 int 1593248
  • 1
  • 2
  • 3

大體過程:定義變量->內存增長->清除變量->內存恢復

潛在的內存變化

當執行:

$name = "咖啡色的羊駝";
  • 1

時候,內存的分配作了兩件事情:1.爲變量名分配內存,存入符號表 2.爲變量值分配內存

再來看代碼:

<?php var_dump(memory_get_usage()); for($i=0;$i<100;$i++) { $a = "test".$i; $$a = "hello"; } var_dump(memory_get_usage()); for($i=0;$i<100;$i++) { $a = "test".$i; unset($$a); } var_dump(memory_get_usage());
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

會獲得:

int 1596864 int 1612080 int 1597680
  • 1
  • 2
  • 3

簡直爆炸,怎麼和以前看的不同?內存沒有所有回收回來。

對於php的核心結構Hashtable來講,因爲未知性,定義的時候不可能一次性分配足夠多的內存塊。因此初始化的時候只會分配一小塊,等不夠的時候在進行擴容,而Hashtable只擴容不減小,因此就出現了上述的狀況:當存入100個變量的時候,符號表不夠用了就進行一次擴容,當unset的時候只釋放了」爲變量值分配內存」,而「爲變量名分配內存」是在符號表的,符號表並無縮小,因此沒收回來的內存是被符號表佔去了。

潛在的內存申請與釋放設計

php和c語言同樣,也是須要進行申請內存的,只不過這些操做做者都封裝到底層了,php使用者無感知而已。

php的內存申請小設計

php並不是簡單的向os申請內存,而是會申請一大塊內存,把其中一部分分給申請者,這樣當再有邏輯來申請內存的時候,就不須要向os申請了,避免了頻繁調用。當內存不夠的時候纔會再次申請

php的內存釋放小設計

當釋放內存的時候,php並不是會把內存還給os,而是把內存軌道本身維護的空閒內存列表,以便重複利用,

php中垃圾是如何定義的?

準確地說,判斷是否爲垃圾,主要看有沒有變量名指向變量容器zval,若是沒有則認爲是垃圾,須要釋放。

打個比方:

<?php $name = "咖啡色的羊駝"; // todo other things
  • 1
  • 2
  • 3

當定義namename就是能夠回收的垃圾了,然而對於引擎來講,$name仍是實打實存在的refcount也仍是1,因此就不是垃圾,不能回收。當調用unset的時候,也並不必定引擎會認爲它是一個垃圾而進行回收,主要仍是看refcount是否是真的變爲0了。

老版本php中如何產生內存泄漏垃圾?

產生內存泄漏主要真兇:環形引用。 
如今來造一個環形引用的場景:

<?php $a = ['one']; $a[] = &$a; xdebug_debug_zval('a');
  • 1
  • 2
  • 3
  • 4

獲得:

a:
(refcount=2, is_ref=1), array (size=2) 0 => (refcount=1, is_ref=0),string 'one' (length=3) 1 => (refcount=2, is_ref=1), &array<
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

這樣 $a數組就有了兩個元素,一個索引爲0,值爲one字符串,另外一個索引爲1,爲$a自身的引用。

圖1

此時刪掉$a:

<?php $a = ['one']; $a[] = &$a; unset($a);
  • 1
  • 2
  • 3
  • 4

圖2

若是在小於php5.3的版本就會出現一個問題:$a已經不在符號表了,沒有變量再指向此zval容器,用戶已沒法訪問,可是因爲數組的refcount變爲1而不是0,致使此部份內存不能被回收從而產生了內存泄漏。

5.3版本之後php是如何處理垃圾內存的?

判斷處理過程

爲解決環形引用致使的垃圾,產生了新的GC算法,遵照如下幾個基本準則:

1.若是一個zval的refcount增長,那麼此zval還在使用,不屬於垃圾

2.若是一個zval的refcount減小到0, 那麼zval能夠被釋放掉,不屬於垃圾

3.若是一個zval的refcount減小以後大於0,那麼此zval還不能被釋放,此zval可能成爲一個垃圾

are you ok?

來個白話文版:就是對此zval中的每一個元素進行一次refcount減1操做,操做完成以後,若是zval的refcount=0,那麼這個zval就是一個垃圾

引用php官方手冊的配圖:

圖3

A:爲了不每次變量的refcount減小的時候都調用GC的算法進行垃圾判斷,此算法會先把全部前面準則3狀況下的zval節點放入一個節點(root)緩衝區(root buffer),而且將這些zval節點標記成紫色,同時算法必須確保每個zval節點在緩衝區中之出現一次。當緩衝區被節點塞滿的時候,GC纔開始開始對緩衝區中的zval節點進行垃圾判斷。

B:當緩衝區滿了以後,算法以深度優先對每個節點所包含的zval進行減1操做,爲了確保不會對同一個zval的refcount重複執行減1操做,一旦zval的refcount減1以後會將zval標記成灰色。須要強調的是,這個步驟中,起初節點zval自己不作減1操做,可是若是節點zval中包含的zval又指向了節點zval(環形引用),那麼這個時候須要對節點zval進行減1操做。

C:算法再次以深度優先判斷每個節點包含的zval的值,若是zval的refcount等於0,那麼將其標記成白色(表明垃圾),若是zval的refcount大於0,那麼將對此zval以及其包含的zval進行refcount加1操做,這個是對非垃圾的還原操做,同時將這些zval的顏色變成黑色(zval的默認顏色屬性)

D:遍歷zval節點,將C中標記成白色的節點zval釋放掉。

are you ok?

來個白話文版的: 
例如:

<?php $a = ['one']; --- zval_a(將$a對應的zval,命名爲zval_a) $a[] = &$a; --- step1 unset($a); --- step2
  • 1
  • 2
  • 3
  • 4

爲進行unset以前(step1),進行算法計算,對這個數組中的全部元素(索引0和索引1)的zval的refcount進行減1操做,因爲索引1對應的就是zval_a,因此這個時候zval_a的refcount應該變成了1,這樣說明zval_a不是一個垃圾不進行回收。

當執行unset的時候(step2),進行算法計算,因爲環形引用,上文得出會有垃圾的結構體,zval_a的refcount是1(zval_a中的索引1指向zval_a),用算法對數組中的全部元素(索引0和索引1)的zval的refcount進行減1操做,這樣zval_a的refcount就會變成0,因而就認爲zval_a是一個須要回收的垃圾。

算法總的套路:對於一個包含環形引用的數組,對數組中包含的每一個元素的zval進行減1操做,以後若是發現數組自身的zval的refcount變成了0,那麼能夠判斷這個數組是一個垃圾。

算法優化配置

可能會發現,每次都進行這樣的操做好像會影響性能,是的,php作事情套路都是走批量的原則。

申請內存也是申請一大塊,僅使用當前的一小部分剩下的等下回再用,避免屢次申請。

這個gc算法也是這樣,會有一個緩衝區的概念,等緩衝區滿了纔會一次性去給清掉。

開關配置

php.ini中設置 zend.enable_gc 項來開啓或則關閉GC。

緩衝區配置

緩衝區默承認以放10,000個節點,當緩衝區滿了纔會清理。能夠經過修改Zend/zend_gc.c中的GC_ROOT_BUFFER_MAX_ENTRIES 來改變這個數值,須要從新編譯連接PHP

關鍵函數

gc_enable() : 開啓GC

gc_disable() : 關閉GC

gc_collect_cycles() : 在節點緩衝區未滿的狀況下強制執行垃圾分析算法

涉及到垃圾回收的知識點

1.unset函數

unset只是斷開一個變量到一塊內存區域的鏈接,同時將該內存區域的引用計數-1;內存是否回收主要仍是看refount是否到0了,以及gc算法判斷。

2.= null 操做;

a=nulla 指向的數據結構置空,同時將其引用計數歸0。

3.腳本執行結束

腳本執行結束,該腳本中使用的全部內存都會被釋放,不管是否有引用環。

相關文章
相關標籤/搜索