PHP 作為托管語言,內存管理由 Zend Engine 內部的 **Zend Memory Manager (ZendMM)** 統一調度。理解 ZendMM 工作原理、引用計數、Copy-on-Write (COW)、Change-on-Write、循環垃圾回收 (GC),對編寫高性能、長運行腳本 (CLI Daemon、Worker、Swoole) 至關重要。
| 系統函數 | ZendMM 對應 | 持久化版本 |
|---|---|---|
| malloc(size) | emalloc(size) | pemalloc(size, persistent) |
| calloc(n, size) | ecalloc(n, size) | pecalloc(n, size, persistent) |
| realloc(ptr, size) | erealloc(ptr, size) | perealloc(ptr, size, persistent) |
| strdup(str) | estrdup(str) | pestrdup(str, persistent) |
| free(ptr) | efree(ptr) | pefree(ptr, persistent) |
每個 `zval` (PHP 變量容器) 內含 `refcount__gc` (引用計數) 和 `is_ref__gc` (是否引用)。
當嘗試**修改**共享 `zval` 且 `refcount > 1` 時,Zend **分離** 一份副本:
<?php
$a = 'hello'; // refcount=1
$b = $a; // refcount=2 (共享)
$b .= ' world'; // 觸發 COW:分離 $b 副本,修改副本
// $a 仍為 'hello', $b 為 'hello world'
?>
使用 `&` 顯式引用賦值:`$b = &$a`。
當同一 `zval` 同時存在 **引用綁定** (`is_ref=1`) 和 **普通共享** (`refcount>1, is_ref=0`),修改任一方均需分離,導致**強制複製**:
<?php
$a = 1;
$b = $a; // 共享, refcount=2
$c = &$a; // 引用, refcount=3, is_ref=1 (衝突!)
// 引擎強制分離:$c 指向原 zval (is_ref=1), $a/$b 指向新副本 (refcount=2, is_ref=0)
$c = 2; // $c=2, $a=1, $b=1
?>
同理 `$a=1; $b=&$a; $c=$a;` 也會強制分離。避免混合引用/非引用共享同一變量。
引用計數無法處理**循環引用** (對象 A→B, B→A)。PHP 5.3 引入**同步 GC**:
| 問題 | 原因 | 解決方案 |
|---|---|---|
| 內存單調增長 | 數據累積在請求級數組/對象,未釋放 | 定期 `unset` 大變量、用生成器流式處理、分批處理後 `gc_collect_cycles()` |
| 循環引用洩漏 | 對象雙向引用、觀察者模式未弱引用 | 用 `WeakReference` (PHP 7.4+)、手動斷開引用、定期 GC |
| 字符串拼接產生大量臨時對象 | `.=` 在循環中反復 COW 分離 | 用數組收集最後 `implode`、或 `yield` 生成器輸出 |
| Composer 自動加載 classmap 佔用大 | 數萬類映射常駐內存 | 生產 `composer dump-autoload -o` (classmap) 比動態查找更省內存 |
memory_get_usage(true):ZendMM 分配總量 (含內部碎片)memory_get_peak_usage(true):峰值內存gc_status() (PHP 7.3+):GC 統計信息