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

PHP 記憶體溢出徹底解決方案:分批處理、unset 機制深度解析與最佳實踐

     編輯:PHP 相關知識

    一、記憶體溢出核心解決策略

    處理大量資料(如 50 萬筆日誌)時,單純增加 memory_limit 只是治標。核心原則是 「分而治之、及時釋放」:

    1. 分批讀取/處理:避免一次將所有資料載入陣列,使用生成器、游標或 array_splice 分塊處理。
    2. 及時 unset() 釋放變數:處理完的中間變數立即銷毀,讓 GC 回收記憶體。
    3. 減少靜態/全域變數:長生命週期變數佔用記憶體不釋放,改用引用傳遞或依賴注入。
    4. 資源及時關閉:資料庫連線、檔案句柄、CURL 句柄、物件使用完畢立即關閉/銷毀。

    實戰範例:分批處理 50 萬筆日誌

    <?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() 釋放記憶體的深層機制實測

    許多開發者誤以為 unset($var) 立即歸還記憶體給作業系統,實際上 PHP 使用 參考計數 與 複製寫入 機制,行為如下:

    實測 1:小字串(255 位元組)

    <?php
    $s = str_repeat('1', 255);
    $m = memory_get_usage();
    unset($s);
    $mm = memory_get_usage();
    echo $m - $mm; // 輸出 0(記憶體未釋放)
    ?>

    實測 2:大字串(256 位元組)

    <?php
    $s = str_repeat('1', 256);
    $m = memory_get_usage();
    unset($s);
    $mm = memory_get_usage();
    echo $m - $mm; // 輸出 272(記憶體已釋放)
    ?>

    結論一:變數值佔用超過 256 位元組時,unset() 才會觸發記憶體釋放(PHP 內部閾值,版本可能略有差異)。

    實測 3:存在引用時 unset 單一變數

    <?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(記憶體未釋放) ?>

    實測 4:銷毀所有引用或設為 null

    <?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(記憶體已釋放) ?>

    結論二:只有當指向該資料的所有符號(變數、引用)皆被銷毀/解除引用,底層 zval 參考計數歸零,記憶體才會真正釋放。

    三、生產環境最佳實踐清單

    場景建議做法避免做法
    大型陣列遍歷使用 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() 逐行、或 SplFileObjectfile() / file_get_contents() 載入整檔
    物件生命週期實作 __destruct() 關閉資源、unset($obj) 後 gc_collect_cycles()依賴 GC 自動回收循環引用物件
    Composer 自動載入生產環境執行 composer dump-autoload -o 生成類別映射開發模式自動載入(每次掃描目錄)

    四、進階工具與監控

    • Xdebug + Webgrind:分析記憶體峰值、函數調用開銷。
    • Memory Profiler (php-memory-profiler):程式碼級記憶體分配追蹤。
    • 生產環境監控:memory_get_peak_usage(true) 記錄至日誌,配合 Grafana/ELK 告警。
    • OPcache:啟用 opcache.enable=1、opcache.memory_consumption 適當調大,減少編譯佔用。
    1. 上一篇文章:
    2. 下一篇文章:
    Copyright © 程式碼之家 All Rights Reserved