PHP 內存洩漏根因與解決:從引用計數到生成器流式處理
PHP 使用**引用計數**管理內存:每個 zval 有 `refcount`,歸零即釋放。但**循環引用**(A→B→A)導致計數永不歸零,需**垃圾回收器 (GC)** 定期掃描清理。理解機制,才能寫出常駐內存穩定的長運行腳本。
核心機制速覽
- **引用計數**:`$a = 'str'; $b = $a;` 字符串 `refcount=2`。`unset($a)` → `refcount=1`,不釋放。
- **Copy-on-Write**:寫入時才真正複製內存,`$b = $a; $b[0] = 'x';` 觸發分離。
- **循環垃圾回收 (GC)**:PHP 5.3+,檢測 `refcount>0` 但不可達的對象/陣列。`gc_enable()`、`gc_collect_cycles()`、`gc_disable()`。
- **生成器**:`yield` 逐行產出,**不載入整個結果集到內存**,解決大數據集 OOM。
- **WeakReference (PHP 7.4+)**:弱引用不增加 `refcount`,解決緩存/觀察者模式循環引用。
實戰 1:大數據導出用生成器
// ❌ 記憶體爆炸
function getAllUsers(): array {
return $pdo->query('SELECT * FROM users')->fetchAll(PDO::FETCH_ASSOC);
}
// ✅ 常駐內存 < 2MB
function getUsersGenerator(): Generator {
$stmt = $pdo->query('SELECT * FROM users');
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
yield $row;
}
}
foreach (getUsersGenerator() as $user) {
fputcsv($fp, $user);
}
實戰 2:手動釋放大陣列/對象
function processLargeDataset(): void {
$data = []; // 假設佔用 200MB
for ($i = 0; $i < 100000; $i++) {
$data[] = generateHeavyObject();
if ($i % 1000 === 0) {
// 處理完一批,顯式釋放
unset($data);
$data = []; // 重新分配新陣列
gc_collect_cycles(); // 強制觸發 GC (可選)
}
}
}
實戰 3:緩存用 WeakReference 避免內存洩漏
class Cache {
private array $refs = [];
public function get(string $key): ?object {
return $this->refs[$key]?->get() ?? null;
}
public function set(string $key, object $value): void {
$this->refs[$key] = WeakReference::create($value);
}
}
// 當外部無強引用時,對象可被 GC 回收,緩存自動失效
實戰 4:Composer 自動加載優化
# 生產環境
composer install --no-dev --classmap-authoritative --optimize-autoloader
# --classmap-authoritative: 不掃描文件系統,直接信任 classmap
# --optimize-autoloader: 生成 class map,PSR-4 查找更快
監控與調試工具
| 工具 | 用途 |
| memory_get_usage(true) | 當前腳本實際分配內存 (含內存管理器開銷) |
| memory_get_peak_usage(true) | 峰值內存 |
| xdebug.memory_profiling | 生成 cachegrind 文件,KCacheGrind/QCacheGrind 可視化 |
| Blackfire.io | 線上性能分析、內存時間線 |
| php -d memory_limit=256M script.php | 臨時調大限制排查 |
常見內存洩漏模式與修復
| 代碼模式 | 問題 | 修復 |
| `$obj->self = $obj;` | 對象自我引用 | 避免,或用 WeakReference |
| `$parent->children[] = $child; $child->parent = $parent;` | 雙向關聯循環引用 | 單向關聯 + 查詢方法,或 WeakReference |
| `foreach ($huge as $k => $v) { $huge[$k] = transform($v); }` | 修改陣列觸發 COW 複製整個陣列 | 用引用 `&$v` 或建立新陣列 |
| 閉包捕獲 `$this` / 大變量 | 閉包持有引用延長生命週期 | 僅捕獲所需標量,或 `use ($var)` 顯式列表 |
| 單例/靜態屬性持有大對象 | 進程生命週期內永不釋放 | 定期清理、弱引用、分批處理 |
長運行腳本 (Daemon/Worker) 內存穩定清單
- 循環體內 `unset` 大變量、`gc_collect_cycles()` 定期調用。
- 使用生成器/迭代器流式處理,避免 `fetchAll`/`file_get_contents` 大文件。
- 啟用 OPcache + JIT,減少編譯內存開銷。
- 監控 `memory_get_usage()` 異常增長時告警。
- 定期重啟 Worker (如 Supervisor `max_requests=1000`),徹底釋放殘留內存。
內存管理不是黑魔法,理解引用計數 + GC + 生成器,即可駕馭任意規模 PHP 應用。
- 上一篇文章: PHP 面向對象克隆深度解析:clone 關鍵字、__clone() 魔術方法、淺拷貝與深拷貝實戰
- 下一篇文章: Poco C++ TCPServer 框架原理深度解析:Windows Select 模型、線程池、反應器模式、關鍵類關係圖
Copyright ©
程式碼之家 All Rights Reserved