PHP 5.3 引入 **同步週期回收** 算法,解決引用計數無法處理的循環引用記憶體洩漏。本文從原始碼層面剖析 zval_gc_info 結構、GC 根緩衝區、四色標記法、gc_collect_cycles() 執行流程,並說明配置與效能調優。
<?php
$a = []; $a[] = &$a; // 陣列包含自己(循環引用)
unset($a); // refcount 從 2 降為 1,不為 0,內存不釋放 → 洩漏
PHP 5.2 及以前:請求結束時統一釋放,長運行 CLI 腳本會無限增長記憶體。
// PHP 5.3 前 struct _zval_struct { zvalue_value value; zend_uint refcount; zend_uchar type; zend_uchar is_ref; }; // PHP 5.3+ 新增 __gc 後綴欄位 struct _zval_struct { zvalue_value value; zend_uint refcount__gc; // 重命名,供 GC 讀寫 zend_uchar type; zend_uchar is_ref__gc; // 重命名 };
typedef struct _zval_gc_info { zval z; // 嵌入原 zval,保證記憶體對齊,可直接強轉 union { gc_root_buffer *buffered; // 在根緩衝區中的節點指標 struct _zval_gc_info *next; // 空閒鏈表下一節點 } u; } zval_gc_info;
sizeof(zval_gc_info),而非 sizeof(zval)ALLOC_ZVAL 宏重定義:分配後呼叫 GC_ZVAL_INIT(z) 初始化 u.buffered = NULLstruct _zend_gc_globals { zend_bool gc_enabled; // 是否啟用 zend_bool gc_active; // 是否正在執行 GC gc_root_buffer *buf; // 預分配緩衝區陣列 (預設 10,000) gc_root_buffer roots; // 根節點雙向鏈表頭 gc_root_buffer *unused; // 空閒節點鏈表 gc_root_buffer *first_unused; // 首個空閒節點 gc_root_buffer *last_unused; // 末尾標記 zval_gc_info *zval_to_free; // 待釋放 zval 臨時鏈表 zval_gc_info *free_list; zval_gc_info *next_to_free; zend_uint gc_runs; // GC 執行次數統計 zend_uint collected; // 回收物件數統計 };
| 顏色 | 常數 | 含義 | 位元值 |
|---|---|---|---|
| 黑色 | GC_BLACK | 正常、非垃圾、未在緩衝區 | 0x00 |
| 白色 | GC_WHITE | 確認垃圾,待釋放 | 0x01 |
| 灰色 | GC_GREY | 已執行一次 refcount -1(DFS 過程中) | 0x02 |
| 紫色 | GC_PURPLE | 已放入根緩衝區 | 0x03 |
位元操作巨集:
#define GC_COLOR 0x03 #define GC_GET_COLOR(v) (((zend_uintptr_t)(v)) & GC_COLOR) #define GC_SET_COLOR(v, c) (v) = ((gc_root_buffer*)((((zend_uintptr_t)(v)) & ~GC_COLOR) | (c))) #define GC_SET_PURPLE(v) GC_SET_COLOR(v, GC_PURPLE)
unset() → zval_ptr_dtor() → Z_DELREF_PP() 遞減 refcountzval_dtor + efree)GC_ZVAL_CHECK_POSSIBLE_ROOT() → gc_zval_possible_root()root_buf_length++gc_collect_cycles()gc_collect_cycles() 可隨時呼叫// 簡化版 zend_gc.c void gc_collect_cycles() { // 步驟 A: 標記根節點為灰色,遞減 refcount (DFS) gc_mark_roots(); // 第 628 行 // 步驟 B: 掃描根緩衝區,refcount == 0 標白,>0 恢復 refcount 標黑 (DFS) gc_scan_roots(); // 第 629 行 // 步驟 C: 遍歷根緩衝區,移除節點,釋放白色標記的 zval gc_collect_roots(); // 第 630 行 // → 白色節點加入 zval_to_free 鏈表 // → 最後統一釋放 }
zval_to_free,最後批次 zval_dtor() + efree(); php.ini
zend.enable_gc = 1 ; 預設開啟
; zend.enable_gc = 0 ; 關閉 GC(不建議,除非極度效能敏感且確無循環引用)
; 運行時控制
gc_enable(); // 開啟
gc_disable(); // 關閉
gc_collect_cycles(); // 強制執行一次週期回收,回傳回收物件數
| 面向 | 影響 | 建議 |
|---|---|---|
| CPU 開銷 | DFS 遍歷、三次掃描 | 長運行 CLI 定期呼叫 gc_collect_cycles()(如每 1000 次迭代),避免緩衝區滿時突發卡頓 |
| 記憶體 | 每 zval 額外 ~16 bytes (64位) | 可接受,換取無洩漏 |
| 根緩衝區大小 | 預設 10,000,硬編碼 GC_ROOT_BUFFER_MAX_ENTRIES | 大量物件專案可考慮修改源碼重編譯,或頻繁手動觸發 |
| 效能分析 | gc_runs, collected 統計 | 生產環境定期抓取 var_dump(gc_status()) (PHP 7.3+) |
Bucket/zend_array,GC 邏輯大幅簡化、效能提升gc_status() 回傳 ['runs'=>..., 'collected'=>..., 'threshold'=>..., 'roots'=>...]libxml_disable_entity_loader 預設關閉、gc_collect_cycles() 回傳型別改為 intgc_collect_cycles()SplObjectStorage + 弱引用、快取用 WeakMap (PHP 8.0+)、父子雙向引用在銷毀時手動斷開gc_runs 急增、collected 異常 → 檢查程式碼新增循環引用理解 GC 內部機制,不只是為了面試,更是為了在記憶體洩漏發生時能精準定位、在高併發長運行服務中做正確的效能權衡。PHP 5.3 的同步週期回收算法,是 PHP 邁向企業級長運行語言的關鍵里程碑。