處理大量資料(如 50 萬筆日誌)時,單純增加 memory_limit 只是治標。核心原則是 「分而治之、及時釋放」:
array_splice 分塊處理。unset() 釋放變數:處理完的中間變數立即銷毀,讓 GC 回收記憶體。<?php
ini_set('memory_limit', '64M'); // 根據實際情況調整,safe_mode 下無效
set_time_limit(600); // 10 分鐘超時
$root = '/Data/webapps/VisitLog';
$month = date('Y-m', $timestamp);
$day = date('m-d', $timestamp);
$spt = '$@#!$'; // 日誌欄位分隔符
$batchSize = 10000; // 每批處理筆數
$farr = []; // 累積當日所有小時檔案內容
// 1. 讀取 24 個小時檔案,合併到 $farr
for ($h = 0; $h < 24; $h++) {
$file = "$root/$month/$day/" . sprintf('%02d', $h) . ".php";
if (!file_exists($file)) continue;
$lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
array_shift($lines); // 移除首行
$farr = array_merge($farr, $lines);
unset($lines); // 立即釋放
}
if (empty($farr)) {
echo "無相關記錄! ";
exit;
}
// 2. 分批處理,提取所需欄位
$Uarr = $Marr = $IParr = [];
while (!empty($farr)) {
$chunk = array_splice($farr, 0, $batchSize); // 取出並移除前 $batchSize 筆
foreach ($chunk as $line) {
$parts = explode($spt, $line);
$Uarr[] = $parts[1]; // vurl
$Marr[] = $parts[2]; // vmark
$IParr[] = $parts[3] . " | " . $parts[1]; // IP + URL
}
unset($chunk); // 釋放當批次記憶體
}
unset($farr); // 釋放總陣列
// 3. 後續統計分析...
echo "處理完成:URL " . count($Uarr) . " 筆,標記 " . count($Marr) . " 筆,IP " . count($IParr) . " 筆。";
?>
許多開發者誤以為 unset($var) 立即歸還記憶體給作業系統,實際上 PHP 使用 參考計數 與 複製寫入 機制,行為如下:
<?php
$s = str_repeat('1', 255);
$m = memory_get_usage();
unset($s);
$mm = memory_get_usage();
echo $m - $mm; // 輸出 0(記憶體未釋放)
?>
<?php
$s = str_repeat('1', 256);
$m = memory_get_usage();
unset($s);
$mm = memory_get_usage();
echo $m - $mm; // 輸出 272(記憶體已釋放)
?>
<?php
$s = str_repeat('1', 256);
$p = &$s; // 建立引用
$m = memory_get_usage();
unset($s); // 僅銷毀 $s,$p 仍指向資料
$mm = memory_get_usage();
echo $p . '
'; // 仍輸出 256 個 1
echo $m - $mm; // 輸出 0(記憶體未釋放)
?>
<?php
$s = str_repeat('1', 256);
$p = &$s;
$m = memory_get_usage();
$s = null; // 或 unset($s); unset($p);
$mm = memory_get_usage();
echo $p . '
'; // 輸出空字串
echo $m - $mm; // 輸出 272(記憶體已釋放)
?>
| 場景 | 建議做法 | 避免做法 |
|---|---|---|
| 大型陣列遍歷 | 使用 foreach ($arr as $k => $v) { ...; unset($arr[$k]); } 或 array_splice 分批 | 一次性 array_map/array_filter 產生新大陣列 |
| 資料庫大量查詢 | 使用 PDO::fetchAll(PDO::FETCH_ASSOC) 分頁、或游標 PDO::CURSOR_SCROLL | 一次 fetchAll() 載入百萬行 |
| 檔案讀取 | fopen() + fgets() 逐行、或 SplFileObject | file() / file_get_contents() 載入整檔 |
| 物件生命週期 | 實作 __destruct() 關閉資源、unset($obj) 後 gc_collect_cycles() | 依賴 GC 自動回收循環引用物件 |
| Composer 自動載入 | 生產環境執行 composer dump-autoload -o 生成類別映射 | 開發模式自動載入(每次掃描目錄) |
memory_get_peak_usage(true) 記錄至日誌,配合 Grafana/ELK 告警。opcache.enable=1、opcache.memory_consumption 適當調大,減少編譯佔用。