首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 內存管理深度解析:Zend Memory Manager、引用計數、寫時複製、垃圾回收

     編輯:PHP 相關知識

PHP 作為托管語言,內存管理由 Zend Engine 內部的 **Zend Memory Manager (ZendMM)** 統一調度。理解 ZendMM 工作原理、引用計數、Copy-on-Write (COW)、Change-on-Write、循環垃圾回收 (GC),對編寫高性能、長運行腳本 (CLI Daemon、Worker、Swoole) 至關重要。

一、Zend Memory Manager (ZendMM) 核心機制

  • 請求級內存隔離:每個 HTTP 請求/CLI 腳本啟動時,ZendMM 分配一塊大內存塊作為「請求堆」。請求結束時,**整體釋放**,無需逐個 `free`,徹底消除請求級內存洩漏。
  • 分配器 API:擴展開發必須使用 ZendMM 宏/函數,而非系統 `malloc/free`:
    系統函數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)
  • 持久化分配:`persistent=1` 走系統 `malloc`,跨請求生存 (如類定義、持久連接);`persistent=0` 走請求堆,請求結束自動釋放。
  • 安全分配:`safe_emalloc(size, count, addtl)` 計算 `(size*count)+addtl` 並檢查整數溢出,防止分配負數/過小內存導致緩衝區溢出。
  • 內存限制:`memory_limit` (php.ini) 由 ZendMM 強制,超限直接 `E_ERROR` 跳出,無需檢查返回值。

二、引用計數

每個 `zval` (PHP 變量容器) 內含 `refcount__gc` (引用計數) 和 `is_ref__gc` (是否引用)。

  • 賦值 `$b = $a`:**增加引用計數**,雙方指向同一 `zval`,**不複製數據**。
  • `unset($a)`:**減少引用計數**,若歸零則釋放 `zval` 及其數據。
  • 此機制極大節省內存:`$big = file_get_contents('100M.log'); $copy = $big;` 僅佔 100M,非 200M。

三、寫時複製 (Copy-on-Write, COW)

當嘗試**修改**共享 `zval` 且 `refcount > 1` 時,Zend **分離** 一份副本:

  1. 檢查 `refcount > 1` 且非 `is_ref`。
  2. 分配新 `zval`,複製數據 (`zval_copy_ctor`)。
  3. 原 `zval` `refcount--`,新 `zval` `refcount=1`、`is_ref=0`。
  4. 修改操作在新副本上進行,原數據不受影響。
<?php
$a = 'hello';  // refcount=1
$b = $a;       // refcount=2 (共享)
$b .= ' world'; // 觸發 COW:分離 $b 副本,修改副本
// $a 仍為 'hello', $b 為 'hello world'
?>

四、改時複製 / 引用賦值 (Change-on-Write)

使用 `&` 顯式引用賦值:`$b = &$a`。

  • 設置 `is_ref = 1`,`refcount++`。
  • 修改 `$b` 時,**不分離**,直接修改共享 `zval`,所有引用方見到變化。
  • 這是 PHP `foreach ($arr as &$v)` 能修改原數組的原因。

五、分離衝突

當同一 `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;` 也會強制分離。避免混合引用/非引用共享同一變量。

六、循環垃圾回收 (GC) - PHP 5.3+

引用計數無法處理**循環引用** (對象 A→B, B→A)。PHP 5.3 引入**同步 GC**:

  • 對象/數組放入「可能根」緩衝區 (默認 10000 個)。
  • 緩衝區滿或手動 `gc_collect_cycles()` 觸發掃描。
  • 標記-掃描算法:從根出發標記可達對象,不可達即循環垃圾,釋放。
  • 性能影響:請求級通常無感;長運行 CLI 需定期 `gc_collect_cycles()` 或調整 `zend.gc_collect_cycles` 閾值。

七、長運行腳本 (CLI Daemon/Swoole) 內存優化實戰

問題原因解決方案
內存單調增長數據累積在請求級數組/對象,未釋放定期 `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 統計信息
  • Xdebug `xdebug_debug_zval()` / `xdebug_debug_zval_stdout()`:查看變量 refcount、is_ref
  • PHP 內置 `zend.memory_manager` 探針 (需編譯啟用)

總結:PHP 開發者的內存心法

  1. **請求級無需手動 free**,ZendMM 包辦。
  2. **賦值不複製**,修改才分離 (COW) —— 善用此特性傳遞大數據。
  3. **引用 (`&`) 打破 COW**,謹慎使用,避免混合共享導致強制分離。
  4. **長運行進程** 必須主動管理:`unset`、生成器、定期 `gc_collect_cycles()`、弱引用。
  5. **擴展開發** 必須用 `emalloc/efree` 等 ZendMM API,持久化用 `pemalloc/pefree`。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved