發佈日期:

電腦越用越慢?分享 PowerShell 腳本,一鍵偵測記憶體與句柄洩漏

Views: 0

🔍 記憶體去哪了?忠碁科技專用診斷工具,教你揪出消失的實體記憶體!

📖 故事的開頭…

最近電腦開機一段時間後,發現記憶體使用了近 90%,但打開工作管理員把所有進程(Processes)的記憶體加總,卻怎麼也湊不到那個數字。

忠碁為了提供相關資訊給其它智慧工具分析,並找出到底是哪個神祕黑洞吃掉了資源,忠碁參考了 網路技術文章 的分析邏輯,編寫了這支「Windows 記憶體耗盡診斷腳本」。它不只看表面數據,更能深入內核層級(Kernel)去偵測那些連工作管理員都抓不到的記憶體佔用。


💡 為什麼你需要這支腳本?(亮點分析)

一般的系統工具只能看到應用程式的佔用,但當你遇到核心層級的問題時,往往無能為力。忠碁科技 在處理企業級系統維護時,常遇到以下「記憶體黑洞」:

  • 驅動程式洩漏 (Driver Leak):非分頁池(Non-Paged Pool)異常飆高,通常是網卡或顯卡驅動作怪。
  • 網路驅動 NDU 衝突:Windows 內置監控導致的記憶體洩漏。
  • 句柄洩漏 (Handle Leak):程式碼沒寫好,瘋狂索取控制權而不歸還。
  • IO 瓶頸:大量修改中頁面(Modified Page)堆積。

✨ 本程式 5 大核心亮點:

  • 🚀 自動權限提升:內建管理員權限請求邏輯,確保獲取內核敏感數據。
  • 🧩 隱形佔用精確計算 ($gap):自動對比系統總量與進程總量,直接算出被核心、快取吃掉的差距(Gap)。
  • 🚨 智慧診斷與預警:
    • 非分頁池預警:自動判別是否超過 1GB/2GB 臨界點,並篩選出可能的第三方驅動。
    • 句柄監控:當單一進程 Handle 超過 10,000 個(例如特定印表機驅動異常),立即觸發警告。
  • 📊 專業級數據報告:執行完畢自動生成 Memory_Report.txt 與 Memory_Summary.txt,格式美觀,方便傳給 忠碁 的技術人員進行深度分析。
  • 🛠️ 實戰派建議:不僅發現問題,更會依據結果引導你使用 poolmon.exe 或 RAMMap 等進階工具。

📝 腳本源碼分享(PowerShell)

請將以下代碼複製並另存為 Memory_Diagnosis.ps1 後執行:

# --- 1. 自動提升權限 ---
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
if (-not $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
    Write-Host ">>> 權限不足,正在請求管理員權限..."
    $argList = "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`""
    try { Start-Process powershell.exe -ArgumentList $argList -Verb RunAs; exit } catch { exit }
}

# --- 2. 路徑設定 ---
$reportPath = "$PSScriptRoot\Memory_Report.txt"
$summaryPath = "$PSScriptRoot\Memory_Summary.txt"
$report = New-Object System.Collections.Generic.List[string]

Write-Host ""
Write-Host "===== 記憶體耗盡診斷 (使用率分析) ====="
Write-Host ""

try {
    # ========== 1. 基礎記憶體數據 ==========
    $os = Get-CimInstance Win32_OperatingSystem
    if ($null -eq $os) { throw "無法取得 Win32_OperatingSystem 資訊" }

    $totalKB = $os.TotalVisibleMemorySize
    $freeKB = $os.FreePhysicalMemory
    $usedKB = $totalKB - $freeKB
    $totalGB = [math]::round($totalKB / 1MB, 2)
    $freeGB = [math]::round($freeKB / 1MB, 2)
    $usedGB = [math]::round($usedKB / 1MB, 2)
    $usagePercent = [math]::round(($usedKB / $totalKB) * 100, 2)
    $bootTime = $os.LastBootUpTime
    $uptime = (Get-Date) - $bootTime

    $report.Add("[1] 系統基本資訊")
    $report.Add("    開機時間  : $bootTime")
    $report.Add("    運行時間  : $($uptime.Days)天 $($uptime.Hours)時 $($uptime.Minutes)分")
    $report.Add("    總記憶體  : $totalGB GB")
    $report.Add("    已用記憶體: $usedGB GB")
    $report.Add("    可用記憶體: $freeGB GB")
    $report.Add("    總體佔用率: $usagePercent %")
    $report.Add("")

    # ========== 2. 核心層級記憶體分類 ==========
    $memPerf = Get-CimInstance Win32_PerfFormattedData_PerfOS_Memory -ErrorAction SilentlyContinue
    if ($null -eq $memPerf) {
        $report.Add("[2] 核心層級記憶體分類 - 警告: 無法讀取效能計數器")
        $nonPagedMB = 0
        $pagedMB = 0
        $cacheMB = 0
        $standbyMB = 0
        $modifiedMB = 0
    } else {
        $nonPagedMB = [math]::round($memPerf.PoolNonpagedBytes / 1MB, 2)
        $pagedMB = [math]::round($memPerf.PoolPagedBytes / 1MB, 2)
        $cacheMB = [math]::round($memPerf.CacheBytes / 1MB, 2)
        $standbyMB = [math]::round($memPerf.StandbyCacheCoreBytes / 1MB, 2)
        $modifiedMB = [math]::round($memPerf.ModifiedPageListBytes / 1MB, 2)

        $report.Add("[2] 核心層級記憶體分類 (找出隱形耗用)")
        $report.Add("    非分頁池 (NonPaged) : $nonPagedMB MB (超過 1000MB 需注意)")
        $report.Add("    分頁池 (Paged)      : $pagedMB MB")
        $report.Add("    系統快取 (Cache)    : $cacheMB MB")
        $report.Add("    待命記憶體 (Standby): $standbyMB MB")
        $report.Add("    修改中頁面 (Modified): $modifiedMB MB")
    }
    $report.Add("")

    # ========== 3. 非分頁池異常診斷 (記憶體洩漏) ==========
    if ($nonPagedMB -gt 2000) {
        $report.Add("[3a] 嚴重警告:非分頁池高達 $nonPagedMB MB (正常值 < 1GB)")
        $report.Add("    這表示驅動程式或核心模組有嚴重記憶體洩漏!")
        $report.Add("")
        
        $drivers = Get-CimInstance Win32_SystemDriver | Where-Object { $_.State -eq "Running" -and $_.PathName -notlike "*\\system32\\drivers\\*" }
        $report.Add("    正在執行的第三方驅動程式 (前20個,可能是元兇):")
        $drivers | Select-Object -First 20 | ForEach-Object {
            $report.Add("        - $($_.Name) : $($_.PathName)")
        }
        if ($drivers.Count -gt 20) {
            $report.Add("        ... 共 $($drivers.Count) 個,已省略 $($drivers.Count - 20) 個")
        }
        $report.Add("")
    }

    # ========== 4. 虛擬記憶體與分頁檔 ==========
    $pageFile = Get-CimInstance Win32_PageFileUsage
    $pageTotalGB = if ($pageFile) { [math]::round($pageFile.AllocatedBaseSize / 1024, 2) } else { 0 }
    $pageUsedGB = if ($pageFile) { [math]::round($pageFile.CurrentUsage / 1024, 2) } else { 0 }
    
    $report.Add("[4] 虛擬記憶體與分頁檔")
    $report.Add("    分頁檔總大小 : $pageTotalGB GB")
    $report.Add("    分頁檔已用   : $pageUsedGB GB")
    $report.Add("    提交總量 (Commit) : $([math]::round($os.TotalAllocatedBaseSize / 1KB / 1024, 2)) GB")
    $report.Add("    提交限制 (Limit)  : $([math]::round($os.TotalVirtualMemorySize / 1MB, 2)) GB")
    $report.Add("")

    # ========== 5. NDU 網路驅動檢查 ==========
    $report.Add("[5] 網路驅動程式與 NDU 狀態")
    
    $ndu = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Ndu" -ErrorAction SilentlyContinue
    $nduStart = if ($ndu) { $ndu.Start } else { "未找到" }
    $report.Add("    NDU 服務啟動類型 : $nduStart (4為禁用,建議設為4)")
    
    $netDrivers = Get-CimInstance Win32_PnPSignedDriver | Where-Object { 
        $_.DeviceName -match "Killer|Realtek|Intel.*Ethernet|Broadcom|VMware|VirtualBox|Hyper-V" -and $_.IsRunning 
    }
    foreach ($net in $netDrivers) {
        $report.Add("    網卡驅動 : $($net.DeviceName) | 版本 : $($net.DriverVersion)")
    }
    $report.Add("")

    # ========== 6. 進程記憶體排行 ==========
    $report.Add("[6] 進程記憶體排行 (WorkingSet - 前 20 名)")
    $report.Add("    名稱                      | 數量 | WorkingSet | 私用記憶體 | 句柄數")
    $report.Add("    ----------------------------------------------------------------")
    
    $processes = Get-Process | Group-Object Name | ForEach-Object {
        $wsSum = ($_.Group | Measure-Object WorkingSet -Sum).Sum
        $pmSum = ($_.Group | Measure-Object PrivateMemorySize -Sum).Sum
        $hdSum = ($_.Group | Measure-Object HandleCount -Sum).Sum
        [PSCustomObject]@{
            Name = $_.Name
            Count = $_.Count
            WorkingSet_MB = [math]::Round($wsSum / 1MB, 2)
            Private_MB = [math]::Round($pmSum / 1MB, 2)
            Handles = $hdSum
        }
    } | Sort-Object WorkingSet_MB -Descending
    
    $processes | Select-Object -First 20 | ForEach-Object {
        $report.Add("    $($_.Name.PadRight(25)) | $($_.Count.ToString().PadLeft(4)) | $($_.WorkingSet_MB.ToString().PadLeft(8)) MB | $($_.Private_MB.ToString().PadLeft(8)) MB | $($_.Handles)")
    }
    
    $totalProcessWS = [math]::Round(($processes | Measure-Object WorkingSet_MB -Sum).Sum, 2)
    $gap = [math]::Round($usedGB * 1024 - $totalProcessWS, 2)
    
    $report.Add("")
    $report.Add("    所有進程 WorkingSet 總和 : $totalProcessWS MB")
    $report.Add("    與系統回報已使用量差距 : $gap MB (這個差距就是核心/驅動/快取吃掉的部分)")
    $report.Add("")

    # ========== 7. Handle 句柄排行 ==========
    $report.Add("[7] 進程句柄排行 (Handles - 前 10 名)")
    $report.Add("    (句柄數異常增加通常代表程式即將耗盡資源)")
    $processes | Sort-Object Handles -Descending | Select-Object -First 10 | ForEach-Object {
        $report.Add("    $($_.Name.PadRight(25)) | 句柄數 (Handles): $($_.Handles)")
    }
    $report.Add("")

    # ========== 8. 句柄洩漏診斷 ==========
    $topHandle = $processes | Sort-Object Handles -Descending | Select-Object -First 1
    $highHandleProcesses = $processes | Where-Object { $_.Handles -gt 10000 } | Sort-Object Handles -Descending
    
    if ($highHandleProcesses.Count -gt 0) {
        $report.Add("[3b] 嚴重警告:發現進程句柄洩漏!")
        foreach ($proc in $highHandleProcesses) {
            $report.Add("    🚨 $($proc.Name) 句柄數高達 $($proc.Handles) (正常值 < 5000)")
        }
        $report.Add("    這可能導致系統資源耗盡,建議採取以下措施:")
        $report.Add("    1. 立即重開機釋放被佔用的句柄")
        $report.Add("    2. 更新或移除對應的驅動程式/軟體")
        $report.Add("    3. 如果是印表機驅動(jcprinter),請更新或解除安裝")
        $report.Add("")
    } elseif ($topHandle.Handles -gt 5000) {
        $report.Add("[3c] 注意:發現句柄數偏高")
        $report.Add("    $($topHandle.Name) 句柄數為 $($topHandle.Handles) (正常值 < 5000)")
        $report.Add("    建議重開機或檢查是否有程式記憶體洩漏")
        $report.Add("")
    }

    # ========== 9. 修改中頁面異常診斷 ==========
    if ($modifiedMB -gt 10000) {
        $report.Add("[3d] 警告:修改中頁面過高 ($modifiedMB MB)")
        $report.Add("    這表示有大量記憶體正在等待寫入分頁檔")
        $report.Add("    建議增加分頁檔大小或檢查磁碟IO效能")
        $report.Add("")
    }

    # ========== 10. 記憶體趨勢警告 ==========
    if ($uptime.TotalHours -gt 48 -and $nonPagedMB -gt 1500) {
        $report.Add("[8] 系統已運行超過 2 天且非分頁池過高 -> 強烈懷疑驅動程式洩漏!")
        $report.Add("")
    }

    # ========== 寫入檔案 ==========
    $report | Out-File -FilePath $reportPath -Encoding utf8
    $processes | Sort-Object WorkingSet_MB -Descending | ForEach-Object {
        "$($_.Name.PadRight(25)) | $($_.Count.ToString().PadLeft(4)) | $($_.WorkingSet_MB.ToString().PadLeft(8)) MB | $($_.Private_MB.ToString().PadLeft(8)) MB | $($_.Handles)"
    } | Out-File -FilePath $summaryPath -Encoding utf8

    Write-Host "分析完成。"
} catch {
    Write-Host "錯誤: $($_.Exception.Message)"
}

🚀 如何使用?

  1. 將上述代碼另存為 Memory_Diagnosis.ps1
  2. 在該檔案上點擊右鍵,選擇「使用 PowerShell 執行」。
  3. 腳本會自動請求權限,並在幾秒內掃描全機狀態。
  4. 執行完成後,請查閱同資料夾下的報告檔案。

💬 結語

在技術服務的領域,數據就是真相。忠碁科技有限公司 始終致力於透過自動化與精準診斷,協助客戶排除最棘手的系統瓶頸。

這支腳本就像是電腦的 X 光機,能幫你把記憶體黑洞照得一清二楚。如果你也深受「記憶體莫名失蹤」所苦,這份邏輯精鍊而成的工具,絕對是你最好的幫手!

💡 小提醒:若診斷報告顯示 [3b] 嚴重警告:發現進程句柄洩漏,且來源是印表機相關驅動,請務必按照報告中的建議更新或重新安裝驅動程式。