## 5.2 垃圾回收
### 5.2.1 垃圾的產生
前面已經介紹過PHP變量的內存管理,即引用計數機制,當變量賦值、傳遞時并不會直接硬拷貝,而是增加value的引用數,unset、return等釋放變量時再減掉引用數,減掉后如果發現refcount變為0則直接釋放value,這是變量的基本gc過程,PHP正是通過這個機制實現的自動垃圾回收,但是有一種情況是這個機制無法解決的,從而因變量無法回收導致內存始終得不到釋放,這種情況就是循環引用,簡單的描述就是變量的內部成員引用了變量自身,比如數組中的某個元素指向了數組,這樣數組的引用計數中就有一個來自自身成員,試圖釋放數組時因為其refcount仍然大于0而得不到釋放,而實際上已經沒有任何外部引用了,這種變量不可能再被使用,所以PHP引入了另外一個機制用來處理變量循環引用的問題。
下面看一個數組循環引用的例子:
```php
$a = [1];
$a[] = &$a;
unset($a);
```
`unset($a)`之前引用關系:

注意這里$a的類型在`&`操作后已經轉為引用,`unset($a)`之后:

可以看到,`unset($a)`之后由于數組中有子元素指向`$a`,所以`refcount = 1`,此時是無法通過正常的gc機制回收的,但是$a已經已經沒有任何外部引用了,所以這種變量就是垃圾,垃圾回收器要處理的就是這種情況,這里明確兩個準則:
>> 1) 如果一個變量value的refcount減少到0, 那么此value可以被釋放掉,不屬于垃圾
>> 2) 如果一個變量value的refcount減少之后大于0,那么此zval還不能被釋放,此zval可能成為一個垃圾
針對第一個情況GC不會處理,只有第二種情況GC才會將變量收集起來。另外變量是否加入垃圾檢查buffer并不是根據zval的類型判斷的,而是與前面介紹的是否用到引用計數一樣通過`zval.u1.type_flag`記錄的,只有包含`IS_TYPE_COLLECTABLE`的變量才會被GC收集。
目前垃圾只會出現在array、object兩種類型中,數組的情況上面已經介紹了,object的情況則是成員屬性引用對象本身導致的,其它類型不會出現這種變量中的成員引用變量自身的情況,所以垃圾回收只會處理這兩種類型的變量。
```c
#define IS_TYPE_COLLECTABLE
```
```c
| type | collectable |
+----------------+-------------+
|simple types | |
|string | |
|interned string | |
|array | Y |
|immutable array | |
|object | Y |
|resource | |
|reference | |
```
### 5.2.2 回收過程
如果當變量的refcount減少后大于0,PHP并不會立即進行對這個變量進行垃圾鑒定,而是放入一個緩沖buffer中,等這個buffer滿了以后(10000個值)再統一進行處理,加入buffer的是變量zend_value的`zend_refcounted_h`:
```c
typedef struct _zend_refcounted_h {
uint32_t refcount; //記錄zend_value的引用數
union {
struct {
zend_uchar type, //zend_value的類型,與zval.u1.type一致
zend_uchar flags,
uint16_t gc_info //GC信息,垃圾回收的過程會用到
} v;
uint32_t type_info;
} u;
} zend_refcounted_h;
```
一個變量只能加入一次buffer,為了防止重復加入,變量加入后會把`zend_refcounted_h.gc_info`置為`GC_PURPLE`,即標為紫色,下次refcount減少時如果發現已經加入過了則不再重復插入。垃圾緩存區是一個雙向鏈表,等到緩存區滿了以后則啟動垃圾檢查過程:遍歷緩存區,再對當前變量的所有成員進行遍歷,然后把成員的refcount減1(如果成員還包含子成員則也進行遞歸遍歷,其實就是深度優先的遍歷),最后再檢查當前變量的引用,如果減為了0則為垃圾。這個算法的原理很簡單,垃圾是由于成員引用自身導致的,那么就對所有的成員減一遍引用,結果如果發現變量本身refcount變為了0則就表明其引用全部來自自身成員。具體的過程如下:
(1) 從buffer鏈表的roots開始遍歷,把當前value標為灰色(zend_refcounted_h.gc_info置為GC_GREY),然后對當前value的成員進行深度優先遍歷,把成員value的refcount減1,并且也標為灰色;
(2) 重復遍歷buffer鏈表,檢查當前value引用是否為0,為0則表示確實是垃圾,把它標為白色(GC_WHITE),如果不為0則排除了引用全部來自自身成員的可能,表示還有外部的引用,并不是垃圾,這時候因為步驟(1)對成員進行了refcount減1操作,需要再還原回去,對所有成員進行深度遍歷,把成員refcount加1,同時標為黑色;
(3) 再次遍歷buffer鏈表,將非GC_WHITE的節點從roots鏈表中刪除,最終roots鏈表中全部為真正的垃圾,最后將這些垃圾清除。
### 5.2.3 垃圾收集的內部實現
接下來我們簡單看下垃圾回收的內部實現,垃圾收集器的全局數據結構:
```c
typedef struct _zend_gc_globals {
zend_bool gc_enabled; //是否啟用gc
zend_bool gc_active; //是否在垃圾檢查過程中
zend_bool gc_full; //緩存區是否已滿
gc_root_buffer *buf; //啟動時分配的用于保存可能垃圾的緩存區
gc_root_buffer roots; //指向buf中最新加入的一個可能垃圾
gc_root_buffer *unused;//指向buf中沒有使用的buffer
gc_root_buffer *first_unused; //指向buf中第一個沒有使用的buffer
gc_root_buffer *last_unused; //指向buf尾部
gc_root_buffer to_free; //待釋放的垃圾
gc_root_buffer *next_to_free;
uint32_t gc_runs; //統計gc運行次數
uint32_t collected; //統計已回收的垃圾數
} zend_gc_globals;
typedef struct _gc_root_buffer {
zend_refcounted *ref; //每個zend_value的gc信息
struct _gc_root_buffer *next;
struct _gc_root_buffer *prev;
uint32_t refcount;
} gc_root_buffer;
```
`zend_gc_globals`是垃圾回收過程中主要用到的一個結構,用來保存垃圾回收器的所有信息,比如垃圾緩存區;`gc_root_buffer`用來保存每個可能是垃圾的變量,它實際就是整個垃圾收集buffer鏈表的元素,當GC收集一個變量時會創建一個`gc_root_buffer`,插入鏈表。
`zend_gc_globals`這個結構中有幾個關鍵成員:
* __(1)buf:__ 前面已經說過,當refcount減少后如果大于0那么就會將這個變量的value加入GC的垃圾緩存區,buf就是這個緩存區,它實際是一塊連續的內存,在GC初始化時一次性分配了10001個gc_root_buffer,插入變量時直接從buf中取出可用節點;
* __(2)roots:__ 垃圾緩存鏈表的頭部,啟動GC檢查的過程就是從roots開始遍歷的;
* __(3)first_unused:__ 指向buf中第一個可用的節點,初始化時這個值為1而不是0,因為第一個gc_root_buffer保留沒有使用,有元素插入roots時如果first_unused還沒有到達buf的尾部則返回first_unused給最新的元素,然后first_unused++,直到last_unused,比如現在已經加入了2個可能的垃圾變量,則對應的結構:

* __(4)last_unused:__ 與first_unused類似,指向buf末尾
* __(5)unused:__ GC收集變量時會依次從buf中獲取可用的gc_root_buffer,這種情況直接取first_unused即可,但是有些變量加入垃圾緩存區之后其refcount又減為0了,這種情況就需要從roots中刪掉,因為它不可能是垃圾,這樣就導致roots鏈表并不是像buf分配的那樣是連續的,中間會出現一些開始加入后面又刪除的節點,這些節點就通過unused串成一個單鏈表,unused指向鏈表尾部,下次有新的變量插入roots時優先使用unused的這些節點,其次才是first_unused的,舉個例子:
```php
//示例1:
$a = array(); //$a -> zend_array(refcount=1)
$b = $a; //$a -> zend_array(refcount=2)
//$b ->
unset($b); //此時zend_array(refcount=1),因為refoucnt>0所以加入gc的垃圾緩存區:roots
unset($a); //此時zend_array(refcount=0)且gc_info為GC_PURPLE,則從roots鏈表中刪掉
```
假如`unset($b)`時插入的是buf中第1個位置,那么`unset($a)`后對應的結構:

如果后面再有變量加入GC垃圾緩存區將優先使用第1個。
此GC機制可以通過php.ini中`zend.enable_gc`設置是否開啟,如果開啟則在php.ini解析后調用`gc_init()`進行GC初始化:
```c
ZEND_API void gc_init(void)
{
if (GC_G(buf) == NULL && GC_G(gc_enabled)) {
//分配buf緩存區內存,大小為GC_ROOT_BUFFER_MAX_ENTRIES(10001),其中第1個保留不被使用
GC_G(buf) = (gc_root_buffer*) malloc(sizeof(gc_root_buffer) * GC_ROOT_BUFFER_MAX_ENTRIES);
GC_G(last_unused) = &GC_G(buf)[GC_ROOT_BUFFER_MAX_ENTRIES];
//進行GC_G的初始化,其中:GC_G(first_unused) = GC_G(buf) + 1;從第2個開始的,第1個保留
gc_reset();
}
}
```
在PHP的執行過程中,如果發現array、object減掉refcount后大于0則會調用`gc_possible_root()`將zend_value的gc頭部加入GC垃圾緩存區:
```c
ZEND_API void ZEND_FASTCALL gc_possible_root(zend_refcounted *ref)
{
gc_root_buffer *newRoot;
//插入的節點必須是GC_BLACK,防止重復插入
ZEND_ASSERT(EXPECTED(GC_REF_GET_COLOR(ref) == GC_BLACK));
newRoot = GC_G(unused); //先看下unused中有沒有可用的
if (newRoot) {
//有的話先用unused的,然后將GC_G(unused)指向單鏈表的下一個
GC_G(unused) = newRoot->prev;
} else if (GC_G(first_unused) != GC_G(last_unused)) {
//unused沒有可用的,且buf中還有可用的
newRoot = GC_G(first_unused);
GC_G(first_unused)++;
} else {
//buf緩存區已滿,這時需要啟動垃圾檢查程序了,遍歷roots,將真正的垃圾釋放
//垃圾回收的動作就是在這觸發的
if (!GC_G(gc_enabled)) {
return;
}
...
//啟動垃圾回收過程
gc_collect_cycles(); //即:zend_gc_collect_cycles()
...
}
//將插入的ref標為紫色,防止重復插入
GC_TRACE_SET_COLOR(ref, GC_PURPLE);
//注意:gc_info不僅僅只有顏色的信息,還會記錄當前gc_root_buffer在整個buf中的位置
//這樣做的目的是可以直接根據zend_value的gc信息取到它的gc_root_buffer,便于進行刪除操作
GC_INFO(ref) = (newRoot - GC_G(buf)) | GC_PURPLE;
newRoot->ref = ref;
//GC_G(roots).next指向新插入的元素
newRoot->next = GC_G(roots).next;
newRoot->prev = &GC_G(roots);
GC_G(roots).next->prev = newRoot;
GC_G(roots).next = newRoot;
}
```
同一個zend_value只會插入一次,再次插入時如果發現其gc_info不是GC_BLACK則直接跳過。另外像上面示例1的情況,插入后如果后面發現其refcount減為0了則表明它可以直接被回收掉,這時需要把這個節點從roots鏈表中刪除,刪除的操作通過`GC_REMOVE_FROM_BUFFER()`宏操作:
```c
#define GC_REMOVE_FROM_BUFFER(p) do { \
zend_refcounted *_p = (zend_refcounted*)(p); \
if (GC_ADDRESS(GC_INFO(_p))) { \
gc_remove_from_buffer(_p); \
} \
} while (0)
ZEND_API void ZEND_FASTCALL gc_remove_from_buffer(zend_refcounted *ref)
{
gc_root_buffer *root;
//GC_ADDRESS就是獲取節點在緩存區中的位置,因為刪除時輸入是zend_refcounted
//而緩存鏈表的節點類型是gc_root_buffer
root = GC_G(buf) + GC_ADDRESS(GC_INFO(ref));
if (GC_REF_GET_COLOR(ref) != GC_BLACK) {
GC_TRACE_SET_COLOR(ref, GC_PURPLE);
}
GC_INFO(ref) = 0;
GC_REMOVE_FROM_ROOTS(root); //雙向鏈表的刪除操作
...
}
```
插入時如果發現垃圾緩存鏈表已經滿了,則會啟動垃圾回收過程:`zend_gc_collect_cycles()`,這個過程會對之前插入緩存區的變量進行判斷是否是循環引用導致的真正的垃圾,如果是垃圾則會進行回收,回收的過程前面已經介紹過:
```c
ZEND_API int zend_gc_collect_cycles(void)
{
...
//(1)遍歷roots鏈表,對當前節點value的所有成員(如數組元素、成員屬性)進行深度優先遍歷把成員refcount減1
gc_mark_roots();
//(2)再次遍歷roots鏈表,檢查各節點當前refcount是否為0,是的話標為白色,表示是垃圾,不是的話需要對還原(1),把refcount再加回去
gc_scan_roots();
//(3)將roots鏈表中的非白色節點刪除,之后roots鏈表中全部是真正的垃圾,將垃圾鏈表轉到to_free等待釋放
count = gc_collect_roots(&gc_flags, &additional_buffer);
...
//(4)釋放垃圾
current = to_free.next;
while (current != &to_free) {
p = current->ref;
GC_G(next_to_free) = current->next;
if ((GC_TYPE(p) & GC_TYPE_MASK) == IS_OBJECT) {
//調用free_obj釋放對象
obj->handlers->free_obj(obj);
...
} else if ((GC_TYPE(p) & GC_TYPE_MASK) == IS_ARRAY) {
//釋放數組
zend_array *arr = (zend_array*)p;
GC_TYPE(arr) = IS_NULL;
zend_hash_destroy(arr);
}
current = GC_G(next_to_free);
}
...
}
```
各步驟具體的操作不再詳細展開,這里單獨說明下value成員的遍歷,array比較好理解,所有成員都在arData數組中,直接遍歷arData即可,如果各元素仍是array、object或者引用則一直遞歸進行深度優先遍歷;object的成員指的成員屬性(不包括靜態屬性、常量,它們屬于類而不屬于對象),前面介紹對象的實現時曾說過,成員屬性除了明確的在類中定義的那些外還可以動態創建,動態屬性保存于zend_obejct->properties哈希表中,普通屬性保存于zend_object.properties_table數組中,這樣以來object的成員就分散在兩個位置,那么遍歷時是分別遍歷嗎?答案是否定的。
實際前面已經簡單提過,在創建動態屬性時會把全部普通屬性也加到zend_obejct->properties哈希表中,指向原zend_object.properties_table中的屬性,這樣一來GC遍歷object的成員時就可以像array那樣遍歷zend_obejct->properties即可,GC獲取object成員的操作由get_gc(即:zend_std_get_gc())完成:
```c
ZEND_API HashTable *zend_std_get_gc(zval *object, zval **table, int *n)
{
if (Z_OBJ_HANDLER_P(object, get_properties) != zend_std_get_properties) {
*table = NULL;
*n = 0;
return Z_OBJ_HANDLER_P(object, get_properties)(object);
} else {
zend_object *zobj = Z_OBJ_P(object);
if (zobj->properties) {
//有動態屬性
*table = NULL;
*n = 0;
return zobj->properties;
} else {
//沒有定義過動態屬性,返回數組
*table = zobj->properties_table;
*n = zobj->ce->default_properties_count;
return NULL;
}
}
}
```
- 前言
- 第1章 PHP基本架構
- 1.1 PHP簡介
- 1.2 PHP7的改進
- 1.3 FPM
- 1.3.1 概述
- 1.3.2 基本實現
- 1.3.3 FPM的初始化
- 1.3.4 請求處理
- 1.3.5 進程管理
- 1.4 PHP執行的幾個階段
- 第2章 變量
- 2.1 變量的內部實現
- 2.2 數組
- 2.3 靜態變量
- 2.4 全局變量
- 2.5 常量
- 第3章 Zend虛擬機
- 3.1 PHP代碼的編譯
- 3.1.1 詞法解析、語法解析
- 3.1.2 抽象語法樹編譯流程
- 3.2 函數實現
- 3.2.1 內部函數
- 3.2.2 用戶函數的實現
- 3.3 Zend引擎執行流程
- 3.3.1 基本結構
- 3.3.2 執行流程
- 3.3.3 函數的執行流程
- 3.3.4 全局execute_data和opline
- 3.4 面向對象實現
- 3.4.1 類
- 3.4.2 對象
- 3.4.3 繼承
- 3.4.4 動態屬性
- 3.4.5 魔術方法
- 3.4.6 類的自動加載
- 3.5 運行時緩存
- 3.6 Opcache
- 3.6.1 opcode緩存
- 3.6.2 opcode優化
- 3.6.3 JIT
- 第4章 PHP基礎語法實現
- 4.1 類型轉換
- 4.2 選擇結構
- 4.3 循環結構
- 4.4 中斷及跳轉
- 4.5 include/require
- 4.6 異常處理
- 第5章 內存管理
- 5.1 Zend內存池
- 5.2 垃圾回收
- 第6章 線程安全
- 6.1 什么是線程安全
- 6.2 線程安全資源管理器
- 第7章 擴展開發
- 7.1 概述
- 7.2 擴展的實現原理
- 7.3 擴展的構成及編譯
- 7.3.1 擴展的構成
- 7.3.2 編譯工具
- 7.3.3 編寫擴展的基本步驟
- 7.3.4 config.m4
- 7.4 鉤子函數
- 7.5 運行時配置
- 7.5.1 全局變量
- 7.5.2 ini配置
- 7.6 函數
- 7.6.1 內部函數注冊
- 7.6.2 函數參數解析
- 7.6.3 引用傳參
- 7.6.4 函數返回值
- 7.6.5 函數調用
- 7.7 zval的操作
- 7.7.1 新生成各類型zval
- 7.7.2 獲取zval的值及類型
- 7.7.3 類型轉換
- 7.7.4 引用計數
- 7.7.5 字符串操作
- 7.7.6 數組操作
- 7.8 常量
- 7.9 面向對象
- 7.9.1 內部類注冊
- 7.9.2 定義成員屬性
- 7.9.3 定義成員方法
- 7.9.4 定義常量
- 7.9.5 類的實例化
- 7.10 資源類型
- 7.11 經典擴展解析
- 7.8.1 Yaf
- 7.8.2 Redis
- 第8章 命名空間
- 8.1 概述
- 8.2 命名空間的定義
- 8.2.1 定義語法
- 8.2.2 內部實現
- 8.3 命名空間的使用
- 8.3.1 基本用法
- 8.3.2 use導入
- 8.3.3 動態用法
- 附錄
- break/continue按標簽中斷語法實現
- defer推遲函數調用語法的實現
- 一起線上事故引發的對PHP超時控制的思考