jvm
環(huán)境準(zhǔn)備
-
確保目標(biāo)機(jī)器上已安裝:
- PowerShell(Windows PowerShell 5.1 或 PowerShell Core 均可)
- JDK(含 jstat, jmap, jcmd, jstack 等工具; 建議 JDK 11+)
-
將以下腳本文件
java_pid_analysis.ps1
(本腳本適用于java22,其他版本要按需調(diào)整命令)放到某固定目錄(如
D:\Scripts\):param( [Parameter(Mandatory=$true)] [int]$targetPid, [Parameter(Mandatory=$true)] [string]$jdkPath ) # Set JDK tools path $jcmdPath = Join-Path -Path $jdkPath -ChildPath "bin\jcmd.exe" $jstatPath = Join-Path -Path $jdkPath -ChildPath "bin\jstat.exe" $jmapPath = Join-Path -Path $jdkPath -ChildPath "bin\jmap.exe" $jstackPath = Join-Path -Path $jdkPath -ChildPath "bin\jstack.exe" # Create analysis report file $reportFile = "jvm_analysis_$targetPid.txt" $currentTime = Get-Date -Format "yyyy-MM-dd HH:mm" Write-Host "==> Inspecting JVM Process PID=$targetPid" -ForegroundColor Cyan Write-Host "==> Using JDK at: $jdkPath" -ForegroundColor Cyan # Initialize report content $reportContent = @" JVM Analysis Report =================== - 時(shí)間: $currentTime - 目標(biāo) PID: $targetPid "@ # 1. OS 層面 CPU/內(nèi)存/線程 Write-Host "`n==> OS Metrics (Get-Process)" -ForegroundColor Cyan $processInfo = Get-Process -Id $targetPid -ErrorAction SilentlyContinue if (-not $processInfo) { Write-Host "Process with PID $targetPid not found!" -ForegroundColor Red exit 1 } $processInfo = $processInfo | Select-Object ` @{Name='CPU(s)';Expression={$_.CPU}}, ` @{Name='WorkingSet(MB)';Expression={[math]::Round($_.WS/1MB,2)}}, ` @{Name='Threads';Expression={$_.Threads.Count}} $processInfo | Format-Table -AutoSize # Add to report $osMetrics = "- OS CPU(s): {0}s; WorkingSet: {1}MB; 線程數(shù): {2}" -f $processInfo.'CPU(s)', $processInfo.'WorkingSet(MB)', $processInfo.'Threads' $reportContent += $osMetrics + "`n" # 2. jstat: GC 與堆使用(每 1s 一次, 共 1 次) Write-Host "`n==> jstat -gcutil" -ForegroundColor Cyan try { $jstatOutput = & $jstatPath -gcutil $targetPid 1000 1 2>&1 $jstatOutput | Out-Host # Parse jstat output (assuming the format is standard) if ($jstatOutput.Count -gt 1) { $jstatValues = $jstatOutput[1] -split '\s+' $jstatLine = "- jstat: E={0}% O={1}% M={2}% CCS={3}% YGC={4} YGCT={5} FGC={6} FGCT={7} GCT={8}" -f $jstatValues[6], $jstatValues[4], $jstatValues[7], $jstatValues[8], $jstatValues[9], $jstatValues[10], $jstatValues[11], $jstatValues[12], $jstatValues[13] $reportContent += $jstatLine + "`n" } } catch { $reportContent += "- jstat: Failed to execute`n" Write-Host "jstat failed: $_" -ForegroundColor Red } # 3. jmap: 堆概要信息 Write-Host "`n==> jmap -heap" -ForegroundColor Cyan try { $jmapOutput = & $jcmdPath $targetPid GC.heap_info 2>&1 $jmapOutput | Out-Host $reportContent += "- jcmd GC.heap_info: `n" + ($jmapOutput -join "`n") + "`n" } catch { $reportContent += "- jmap: Failed to execute`n" Write-Host "jmap failed: $_" -ForegroundColor Red } # 4. jcmd: Native Memory Summary(JDK14+) Write-Host "`n==> jcmd VM.native_memory summary" -ForegroundColor Cyan try { $nativeMemOutput = & $jcmdPath $targetPid VM.flags 2>&1 $nativeMemOutput | Out-Host if ($nativeMemOutput -match "NativeMemoryTracking") { $nmtStatus = $matches[0] $reportContent += "- NativeMemoryTracking 狀態(tài): $nmtStatus`n" } else { $reportContent += "- NativeMemoryTracking 未啟用`n" } } catch { $reportContent += "- native_memory: Failed to execute`n" Write-Host "jcmd failed: $_" -ForegroundColor Red } # 5. JVM 線程數(shù) Write-Host "`n==> jcmd Thread.print (count)" -ForegroundColor Cyan try { $threadCountOutput = & $jcmdPath $targetPid Thread.print 2>&1 $threadCount = $threadCountOutput | Select-String "Total threads:" -SimpleMatch $threadCount | Out-Host if ($threadCount) { $reportContent += "- 線程總數(shù): " + ($threadCount -replace ".*Total threads:\s*", "") + "`n" } else { $reportContent += "- 線程總數(shù): Could not determine`n" } } catch { $reportContent += "- 線程總數(shù): Failed to execute`n" Write-Host "Thread count failed: $_" -ForegroundColor Red } # 6. 線程 Dump(含鎖信息) $dumpFile = "thread_dump_$targetPid.txt" Write-Host "`n==> Generating thread dump to $dumpFile" -ForegroundColor Cyan try { & $jstackPath -l $targetPid 2>&1 > $dumpFile Write-Host " Thread dump saved." -ForegroundColor Green $reportContent += "- 線程 dump見: $dumpFile`n" } catch { $reportContent += "- 線程 dump: Failed to generate`n" Write-Host "Thread dump failed: $_" -ForegroundColor Red } # Save the report $reportContent | Out-File -FilePath $reportFile -Encoding UTF8 Write-Host "`n==> Analysis report saved to $reportFile" -ForegroundColor Green
解鎖并允許腳本執(zhí)行
現(xiàn)場機(jī)器常會因 PowerShell 執(zhí)行策略阻止腳本運(yùn)行,
推薦使用「方法二」一次性繞過:
腳本需要兩個(gè)參數(shù):
- 第一個(gè)參數(shù),pid
- 第二個(gè)參數(shù), jdk路徑,注意到JAVAHOME路徑.
powershell.exe -ExecutionPolicy Bypass -File "D:\Scripts\java_pid_analysis.ps1" 12345 cd-to-java-path
- 僅對本次執(zhí)行生效, 不改動機(jī)器配置.
- 若要長期使用, 可參考下面" 附錄" 修改執(zhí)行策略.
執(zhí)行腳本在控制臺的輸出可能如下:
PS D:\scada\project\預(yù)灌裝NEW> .\check-jvm-by-pid.ps1 21932 D:\scada\chutian-front\front\scada\dist\win-unpacked\resources\app\build\electron\env\jdk-22.0.2 > console_log.txt
==> Inspecting JVM Process PID=21932
==> Using JDK at: D:\scada\chutian-front\front\scada\dist\win-unpacked\resources\app\build\electron\env\jdk-22.0.2
==> OS Metrics (Get-Process)
==> jstat -gcutil
S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT
- 98.33 0.53 96.19 97.11 92.64 573 44.998 50 8.887 304 1.127 55.013
==> jmap -heap
21932:
garbage-first heap total reserved 4184064K, committed 4184064K, used 2601112K [0x0000000700a00000, 0x0000000800000000)
region size 2048K, 30 young (61440K), 29 survivors (59392K)
Metaspace used 109818K, committed 113088K, reserved 1179648K
class space used 21345K, committed 23040K, reserved 1048576K
==> jcmd VM.native_memory summary
21932:
-XX:CICompilerCount=4 -XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=8 -XX:G1EagerReclaimRemSetThreshold=16 -XX:G1HeapRegionSize=2097152 -XX:G1RemSetArrayOfCardsEntries=16 -XX:G1RemSetHowlMaxNumBuckets=8 -XX:G1RemSetHowlNumBuckets=8 -XX:InitialHeapSize=268435456 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=4284481536 -XX:MaxNewSize=2569011200 -XX:MinHeapDeltaBytes=2097152 -XX:MinHeapSize=8388608 -XX:NonNMethodCodeHeapSize=5839372 -XX:NonProfiledCodeHeapSize=122909434 -XX:ProfiledCodeHeapSize=122909434 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:SoftMaxHeapSize=4284481536 -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation
==> jcmd Thread.print (count)
==> Generating thread dump to thread_dump_21932.txt
Thread dump saved.
==> Analysis report saved to jvm_analysis_21932.txt
腳本產(chǎn)物
threaddump21932.txt jstack的結(jié)果
-
jvmanalysis21932.txt 匯總分析,內(nèi)容如下:
- 時(shí)間: 2025-06-16 14:46 - 目標(biāo) PID: 21932 - OS CPU(s): 2228s; WorkingSet: 3868.61MB; 線程數(shù): 136 - jstat: E=92.64% O=96.19% M=573% CCS=44.998% YGC=50 YGCT=8.887 FGC=304 FGCT=1.127 GCT=55.013 - jcmd GC.heap_info: 21932: garbage-first heap total reserved 4184064K, committed 4184064K, used 2601112K [0x0000000700a00000, 0x0000000800000000) region size 2048K, 30 young (61440K), 29 survivors (59392K) Metaspace used 109818K, committed 113088K, reserved 1179648K class space used 21345K, committed 23040K, reserved 1048576K - NativeMemoryTracking Native分配內(nèi)存: Could not determine - 現(xiàn)成dump見: thread_dump_21932.txt
標(biāo)準(zhǔn)診斷流程
按照以下流程, 讓現(xiàn)場同事快速上手:
-
確認(rèn)目標(biāo) PID
-
使用任務(wù)管理器或命令行:
Get-Process -Name java | Format-Table Id, ProcessName, CPU, WS, StartTime 選定要診斷的 JVM 進(jìn)程的 PID.
-
-
執(zhí)行診斷腳本
-
在 PowerShell 中運(yùn)行:
cd D:\Scripts\ powershell.exe -ExecutionPolicy Bypass -File .\java_pid_analysis.ps1 -pid <目標(biāo)PID> -
例如:
powershell.exe -ExecutionPolicy Bypass -File .\java_pid_analysis.ps1 -pid 9876
-
-
查看輸出結(jié)果
- OS Metrics: CPU(s), WorkingSet(MB), 線程數(shù).
- jstat -gcutil: 堆各代使用率及 GC 次數(shù)/耗時(shí).
- jmap -heap: 詳細(xì)堆分代信息, GC 算法.
- jcmd VM.nativememory summary: Native 內(nèi)存分配.
- jcmd Thread.print (count): JVM 內(nèi)部線程總數(shù).
- threaddump_.txt: 完整線程棧(含鎖信息), 保存在腳本目錄.
-
問題定位
- 若 CPU 過高: 定位到哪個(gè)線程占用最高, 可在
thread_dump
中搜索nid=0x...對應(yīng)十進(jìn)制線程 ID, 然后結(jié)合操作系統(tǒng)工具(如
Process Explorer)查找. - 若 內(nèi)存 泄漏: 關(guān)注
jmap -heap中 Old Gen 使用率持續(xù)上升,
或jcmd native_memory中 C 堆本地分配異常. - 若 線程阻塞/死鎖: 在
thread_dump中搜索BLOCKED,
WAITING,Deadlock.
- 若 CPU 過高: 定位到哪個(gè)線程占用最高, 可在
-
匯報(bào)模板 現(xiàn)場診斷完成后, 可用如下模板整理匯報(bào):
- 時(shí)間: 2025-06-16 14:30 - 目標(biāo) PID: 9876 - OS CPU(s): 120.34s; WorkingSet: 1024MB; 線程數(shù): 250 - jstat: E=45.67% O=23.45% M=78.90% ... - jmap -heap 摘要: OldGen=512MB/1024MB (50%), GC 算法: G1 - native_memory summary: C 堆 300MB; 代碼緩存 50MB; 線程棧 200MB - 線程 dump: 見 thread_dump_9876.txt - 初步結(jié)論: OldGen 使用過高 + 多線程阻塞于 XX 鎖, 建議...
附錄: 修改執(zhí)行策略(可選)
如果希望不帶 -ExecutionPolicy 參數(shù)即可運(yùn)行, 可臨時(shí)提升用戶策略:
# 以管理員身份啟動 PowerShell 后執(zhí)行
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
僅本用戶生效, 本地腳本可直接執(zhí)行, 網(wǎng)絡(luò)下載腳本需簽名.
這樣, 一套從" 獲取 PID → 運(yùn)行腳本 → 查看結(jié)果 → 問題定位 → 匯報(bào)"
的流程就完成了, 現(xiàn)場同事照流程走即可快速精準(zhǔn)地收集 JVM 運(yùn)行態(tài)信息.
.Net
環(huán)境準(zhǔn)備
-
確保目標(biāo)機(jī)器上已安裝:
- PowerShell (Windows PowerShell 5.1 或 PowerShell Core 均可)
- .NET SDK (包含 dotnet CLI 工具)
- 診斷工具 (dotnet-trace, dotnet-dump, dotnet-stack 等)
-
安裝必要的診斷工具:
# 安裝 dotnet-trace 工具 dotnet tool install --global dotnet-trace # 安裝 dotnet-dump 工具 dotnet tool install --global dotnet-dump # 安裝 dotnet-stack 工具 dotnet tool install --global dotnet-stack -
將以下腳本文件
dotnet-performance-analysis.ps1放到某固定目錄(如
D:\Scripts\):param( [Parameter(Mandatory=$true)] [int]$targetPid, [string]$outputPath = "." ) # 創(chuàng)建輸出目錄(如果不存在) if (-not (Test-Path -Path $outputPath)) { New-Item -ItemType Directory -Path $outputPath | Out-Null } # 創(chuàng)建分析報(bào)告文件 $reportFile = Join-Path -Path $outputPath -ChildPath "dotnet_analysis_$targetPid.txt" $currentTime = Get-Date -Format "yyyy-MM-dd HH:mm" Write-Host "==> 檢查 .NET 進(jìn)程 PID=$targetPid" -ForegroundColor Cyan # 初始化報(bào)告內(nèi)容 $reportContent = @" .NET 進(jìn)程分析報(bào)告 =================== - 時(shí)間: $currentTime - 目標(biāo) PID: $targetPid "@ # 1. OS 層面 CPU/內(nèi)存/線程 Write-Host "`n==> OS 指標(biāo) (Get-Process)" -ForegroundColor Cyan $processInfo = Get-Process -Id $targetPid -ErrorAction SilentlyContinue if (-not $processInfo) { Write-Host "未找到 PID $targetPid 的進(jìn)程!" -ForegroundColor Red exit 1 } $processInfo = $processInfo | Select-Object ` @{Name='CPU(s)';Expression={$_.CPU}}, ` @{Name='WorkingSet(MB)';Expression={[math]::Round($_.WS/1MB,2)}}, ` @{Name='PrivateMemory(MB)';Expression={[math]::Round($_.PM/1MB,2)}}, ` @{Name='VirtualMemory(MB)';Expression={[math]::Round($_.VM/1MB,2)}}, ` @{Name='Threads';Expression={$_.Threads.Count}}, ` @{Name='Handles';Expression={$_.HandleCount}} $processInfo | Format-Table -AutoSize # 添加到報(bào)告 $osMetrics = "- OS CPU(s): {0}s; WorkingSet: {1}MB; 專用內(nèi)存: {2}MB; 虛擬內(nèi)存: {3}MB; 線程數(shù): {4}; 句柄數(shù): {5}" -f $processInfo.'CPU(s)', $processInfo.'WorkingSet(MB)', $processInfo.'PrivateMemory(MB)', $processInfo.'VirtualMemory(MB)', $processInfo.'Threads', $processInfo.'Handles' $reportContent += $osMetrics + "`n" # 2. 檢查是否安裝了 dotnet-trace 工具 Write-Host "`n==> 檢查 dotnet-trace 工具" -ForegroundColor Cyan try { $dotnetTraceVersion = dotnet tool list --global | Select-String "dotnet-trace" if (-not $dotnetTraceVersion) { Write-Host "未安裝 dotnet-trace 工具,無法收集跟蹤信息" -ForegroundColor Yellow $reportContent += "- dotnet-trace: 未安裝,無法收集跟蹤信息`n" } else { Write-Host "已安裝 $dotnetTraceVersion" -ForegroundColor Green $reportContent += "- dotnet-trace: $dotnetTraceVersion`n" # 收集 CPU 采樣跟蹤(5秒) $traceFile = Join-Path -Path $outputPath -ChildPath "trace_$targetPid.nettrace" Write-Host "`n==> 收集 CPU 采樣跟蹤 (5秒)" -ForegroundColor Cyan try { & dotnet-trace collect --process-id $targetPid --profile cpu-sampling --duration 00:00:05 --output $traceFile $reportContent += "- CPU 采樣跟蹤: $traceFile`n" # 轉(zhuǎn)換為 SpeedScope 格式 $speedscopeFile = Join-Path -Path $outputPath -ChildPath "trace_$targetPid.speedscope.json" Write-Host "`n==> 轉(zhuǎn)換為 SpeedScope 格式" -ForegroundColor Cyan & dotnet-trace convert $traceFile --format speedscope --output $speedscopeFile $reportContent += "- SpeedScope 文件: $speedscopeFile (可在 https://www.speedscope.app/ 查看)`n" } catch { $reportContent += "- CPU 采樣跟蹤: 收集失敗 - $_`n" Write-Host "跟蹤收集失敗: $_" -ForegroundColor Red } } } catch { $reportContent += "- dotnet-trace: 檢查失敗 - $_`n" Write-Host "dotnet-trace 檢查失敗: $_" -ForegroundColor Red } # 3. 檢查是否安裝了 dotnet-dump 工具 Write-Host "`n==> 檢查 dotnet-dump 工具" -ForegroundColor Cyan try { $dotnetDumpVersion = dotnet tool list --global | Select-String "dotnet-dump" if (-not $dotnetDumpVersion) { Write-Host "未安裝 dotnet-dump 工具,無法收集內(nèi)存轉(zhuǎn)儲" -ForegroundColor Yellow $reportContent += "- dotnet-dump: 未安裝,無法收集內(nèi)存轉(zhuǎn)儲`n" } else { Write-Host "已安裝 $dotnetDumpVersion" -ForegroundColor Green $reportContent += "- dotnet-dump: $dotnetDumpVersion`n" # 收集內(nèi)存轉(zhuǎn)儲 $dumpFile = Join-Path -Path $outputPath -ChildPath "dump_$targetPid.dmp" Write-Host "`n==> 收集內(nèi)存轉(zhuǎn)儲 (Mini 類型)" -ForegroundColor Cyan try { & dotnet-dump collect --process-id $targetPid --type Mini --output $dumpFile $reportContent += "- 內(nèi)存轉(zhuǎn)儲: $dumpFile (使用 'dotnet-dump analyze $dumpFile' 分析)`n" } catch { $reportContent += "- 內(nèi)存轉(zhuǎn)儲: 收集失敗 - $_`n" Write-Host "內(nèi)存轉(zhuǎn)儲收集失敗: $_" -ForegroundColor Red } } } catch { $reportContent += "- dotnet-dump: 檢查失敗 - $_`n" Write-Host "dotnet-dump 檢查失敗: $_" -ForegroundColor Red } # 4. 檢查是否安裝了 dotnet-stack 工具 Write-Host "`n==> 檢查 dotnet-stack 工具" -ForegroundColor Cyan try { $dotnetStackVersion = dotnet tool list --global | Select-String "dotnet-stack" if (-not $dotnetStackVersion) { Write-Host "未安裝 dotnet-stack 工具,無法收集堆棧跟蹤" -ForegroundColor Yellow $reportContent += "- dotnet-stack: 未安裝,無法收集堆棧跟蹤`n" } else { Write-Host "已安裝 $dotnetStackVersion" -ForegroundColor Green $reportContent += "- dotnet-stack: $dotnetStackVersion`n" # 收集堆棧跟蹤 $stackFile = Join-Path -Path $outputPath -ChildPath "stack_$targetPid.txt" Write-Host "`n==> 收集堆棧跟蹤" -ForegroundColor Cyan try { & dotnet-stack report --process-id $targetPid > $stackFile $reportContent += "- 堆棧跟蹤: $stackFile`n" } catch { $reportContent += "- 堆棧跟蹤: 收集失敗 - $_`n" Write-Host "堆棧跟蹤收集失敗: $_" -ForegroundColor Red } } } catch { $reportContent += "- dotnet-stack: 檢查失敗 - $_`n" Write-Host "dotnet-stack 檢查失敗: $_" -ForegroundColor Red } # 保存報(bào)告 $reportContent | Out-File -FilePath $reportFile -Encoding UTF8 Write-Host "`n==> 分析報(bào)告已保存到 $reportFile" -ForegroundColor Green # 提供分析建議 Write-Host "`n==> 分析建議:" -ForegroundColor Cyan Write-Host "1. 查看 CPU 采樣跟蹤: 在 https://www.speedscope.app/ 上傳 $speedscopeFile 文件" -ForegroundColor White Write-Host "2. 分析內(nèi)存轉(zhuǎn)儲: 使用命令 'dotnet-dump analyze $dumpFile'" -ForegroundColor White Write-Host "3. 查看堆棧跟蹤: 打開 $stackFile 文件" -ForegroundColor White
解鎖并允許腳本執(zhí)行
現(xiàn)場機(jī)器常會因 PowerShell 執(zhí)行策略阻止腳本運(yùn)行,
推薦使用「方法二」一次性繞過:
powershell.exe -ExecutionPolicy Bypass -File "D:\Scripts\dotnet-performance-analysis.ps1" -targetPid 12345
- 僅對本次執(zhí)行生效, 不改動機(jī)器配置.
- 若要長期使用, 可參考下面"附錄"修改執(zhí)行策略.
診斷工具詳解
dotnet-trace: 性能跟蹤與火焰圖
dotnet-trace 是一個(gè)跨平臺的 .NET 性能分析工具,可以收集運(yùn)行中的 .NET
應(yīng)用程序的性能數(shù)據(jù)。
-
安裝
dotnet tool install -g dotnet-trace -
基本用法
-
查看可用進(jìn)程:
dotnet-trace ps -
收集性能數(shù)據(jù):
# 使用 CPU 采樣配置文件收集 5 秒的跟蹤 dotnet-trace collect -p <PID> --profile cpu-sampling --duration 00:00:05 -
轉(zhuǎn)換為火焰圖格式:
# 將結(jié)果轉(zhuǎn)換為火焰圖格式 dotnet-trace convert <trace-file>.nettrace --format speedscope -
查看火焰圖:
- 訪問 https://www.speedscope.app/
- 上傳生成的 .speedscope.json 文件
-
-
常用配置文件
配置文件名稱 說明 `cpu-sampling` 收集 CPU 采樣數(shù)據(jù),適合分析 CPU 使用率高的問題 `gc-collect` 收集垃圾回收相關(guān)事件 `gc-verbose` 收集詳細(xì)的垃圾回收信息 `http` 收集 HTTP 請求相關(guān)事件 `metrics-browser` 收集瀏覽器指標(biāo)
dotnet-dump: 內(nèi)存轉(zhuǎn)儲分析
dotnet-dump 是一個(gè)用于收集和分析 .NET
應(yīng)用程序內(nèi)存轉(zhuǎn)儲的工具,可以幫助診斷內(nèi)存泄漏、死鎖等問題。
-
安裝
dotnet tool install -g dotnet-dump -
基本用法
-
收集內(nèi)存轉(zhuǎn)儲:
dotnet-dump collect -p <PID> -
分析內(nèi)存轉(zhuǎn)儲:
dotnet-dump analyze <dump-file>.dmp
-
-
常用分析命令
在 dotnet-dump analyze 交互式會話中,可以使用以下命令:
命令 說明 `dumpheap` 顯示堆上的對象信息 `gcroot` 查找對象的引用路徑 `threads` 顯示所有線程信息 `clrstack` 顯示托管代碼的堆棧跟蹤 `dso` 顯示當(dāng)前堆棧邊界內(nèi)的所有托管對象 `dumpobj` 顯示對象的詳細(xì)信息 `dumparray` 顯示數(shù)組的詳細(xì)信息 `dumpasync` 顯示異步狀態(tài)機(jī)信息 `exit` 退出分析會話
dotnet-stack: 堆棧跟蹤
dotnet-stack 是一個(gè)用于收集 .NET
應(yīng)用程序線程堆棧跟蹤的工具,可以幫助診斷線程阻塞、死鎖等問題。
-
安裝
dotnet tool install -g dotnet-stack -
基本用法
-
查看可用進(jìn)程:
dotnet-stack ps -
收集堆棧跟蹤:
dotnet-stack report -p <PID> -
收集到文件:
dotnet-stack report -p <PID> > stack.txt
-
標(biāo)準(zhǔn)診斷流程
按照以下流程,讓現(xiàn)場同事快速上手:
-
確認(rèn)目標(biāo) PID
-
使用任務(wù)管理器或命令行:
Get-Process -Name *dotnet* | Format-Table Id, ProcessName, CPU, WS, StartTime 選定要診斷的 .NET 進(jìn)程的 PID。
-
-
執(zhí)行診斷腳本
-
在 PowerShell 中運(yùn)行:
cd D:\Scripts\ powershell.exe -ExecutionPolicy Bypass -File .\dotnet-performance-analysis.ps1 -targetPid <目標(biāo)PID> -
例如:
powershell.exe -ExecutionPolicy Bypass -File .\dotnet-performance-analysis.ps1 -targetPid 9876
-
-
查看輸出結(jié)果
-
OS Metrics: CPU(s), WorkingSet(MB), 專用內(nèi)存(MB),
虛擬內(nèi)存(MB), 線程數(shù), 句柄數(shù)。 - dotnet-trace: CPU 采樣跟蹤文件和 SpeedScope 文件。
- dotnet-dump: 內(nèi)存轉(zhuǎn)儲文件。
- dotnet-stack: 堆棧跟蹤文件。
-
OS Metrics: CPU(s), WorkingSet(MB), 專用內(nèi)存(MB),
-
問題定位
- 若 CPU 過高:使用 SpeedScope 查看火焰圖,定位熱點(diǎn)方法。
- 若 內(nèi)存 泄漏:使用 dotnet-dump analyze
分析內(nèi)存轉(zhuǎn)儲,查找大對象和引用路徑。 - 若 *線程阻塞/死鎖*:在堆棧跟蹤中查找 BLOCKED 或 WAITING
狀態(tài)的線程。
-
匯報(bào)模板 現(xiàn)場診斷完成后,可用如下模板整理匯報(bào):
- 時(shí)間: 2025-06-16 14:30 - 目標(biāo) PID: 9876 - OS CPU(s): 120.34s; WorkingSet: 1024MB; 專用內(nèi)存: 800MB; 虛擬內(nèi)存: 2048MB; 線程數(shù): 45; 句柄數(shù): 1200 - CPU 熱點(diǎn): 方法 A (30%), 方法 B (15%), 方法 C (10%) - 內(nèi)存分析: 發(fā)現(xiàn)類型 X 的實(shí)例過多 (10000+),可能存在內(nèi)存泄漏 - 線程狀態(tài): 3 個(gè)線程阻塞在 Y 鎖上,可能存在死鎖 - 初步結(jié)論: 內(nèi)存使用過高 + 線程阻塞,建議優(yōu)化 X 類型的對象生命周期管理
常見問題分析與解決方案
CPU 使用率過高
-
癥狀
- 應(yīng)用響應(yīng)緩慢
- 任務(wù)管理器顯示 CPU 使用率高
- 服務(wù)器負(fù)載高
-
分析方法
-
使用 dotnet-trace 收集 CPU 采樣數(shù)據(jù):
dotnet-trace collect -p <PID> --profile cpu-sampling 轉(zhuǎn)換為 SpeedScope 格式并在 https://www.speedscope.app/ 查看
在火焰圖中尋找占用 CPU 時(shí)間最多的方法(最寬的部分)
-
-
常見原因與解決方案
- *無限循環(huán)或低效算法*:優(yōu)化算法,避免不必要的循環(huán)
- *過度分配/回收對象*:使用對象池,減少臨時(shí)對象創(chuàng)建
- *不必要的字符串操作*:使用 StringBuilder,避免頻繁字符串拼接
- *線程爭用*:減少鎖的粒度,使用更高效的并發(fā)模式
內(nèi)存泄漏
-
癥狀
- 應(yīng)用內(nèi)存使用隨時(shí)間持續(xù)增長
- 頻繁的垃圾回收
- 最終導(dǎo)致 OutOfMemoryException
-
分析方法
-
使用 dotnet-dump 收集內(nèi)存轉(zhuǎn)儲:
dotnet-dump collect -p <PID> -
分析內(nèi)存轉(zhuǎn)儲:
dotnet-dump analyze <dump-file>.dmp -
使用以下命令查找大對象:
dumpheap -stat # 按類型統(tǒng)計(jì)對象 dumpheap -type <TypeName> # 查看特定類型的所有對象 gcroot <address> # 查找對象的引用路徑
-
-
常見原因與解決方案
- *事件訂閱未取消*:確保取消事件訂閱,使用弱引用事件模式
- *靜態(tài)集合持有對象*:避免在靜態(tài)集合中存儲長生命周期對象
- *未釋放的資源*:正確實(shí)現(xiàn) IDisposable 模式,使用 using 語句
- *緩存未設(shè)置上限*:使用 MemoryCache 并設(shè)置適當(dāng)?shù)倪^期策略
線程死鎖
-
癥狀
- 應(yīng)用掛起或無響應(yīng)
- 某些操作永遠(yuǎn)不會完成
- 線程數(shù)持續(xù)增長
-
分析方法
-
使用 dotnet-stack 收集堆棧跟蹤:
dotnet-stack report -p <PID> > stack.txt 在堆棧跟蹤中查找 BLOCKED 或 WAITING 狀態(tài)的線程
分析線程之間的依賴關(guān)系,尋找循環(huán)依賴
-
-
常見原因與解決方案
- *嵌套鎖*:避免在持有一個(gè)鎖的同時(shí)獲取另一個(gè)鎖
- *不一致的鎖順序*:確保始終以相同的順序獲取多個(gè)鎖
- *同步上下文死鎖*:使用 ConfigureAwait(false)
避免上下文切換死鎖 - *資源爭用*:使用更細(xì)粒度的鎖,或考慮無鎖數(shù)據(jù)結(jié)構(gòu)
實(shí)際案例分析
案例一:線程數(shù)不斷增長
-
問題描述
應(yīng)用程序運(yùn)行一段時(shí)間后,線程數(shù)不斷增長,最終導(dǎo)致系統(tǒng)資源耗盡。
-
分析過程
- 使用 dotnet-stack 收集堆棧跟蹤
- 發(fā)現(xiàn)大量線程處于等待狀態(tài),且都在相似的代碼位置
- 檢查日志發(fā)現(xiàn)在數(shù)據(jù)采集部分和觸發(fā)數(shù)據(jù)積累的地方都打印了大量日志
-
解決方案
- 刪除采集部分的冗余日志
- 優(yōu)化日志級別,減少不必要的日志輸出
- 修改后線程數(shù)不再隨時(shí)間增長,系統(tǒng)能正常歸檔數(shù)據(jù)
案例二:CPU 使用率突然飆升
-
問題描述
生產(chǎn)環(huán)境中的應(yīng)用在特定操作后 CPU 使用率突然飆升到 100%。
-
分析過程
- 使用 dotnet-trace 收集 CPU 采樣數(shù)據(jù)并生成火焰圖
- 在火焰圖中發(fā)現(xiàn)某個(gè)數(shù)據(jù)處理方法占用了大量 CPU 時(shí)間
- 代碼審查發(fā)現(xiàn)該方法中存在 O(n2) 復(fù)雜度的算法
-
解決方案
- 重構(gòu)算法,將復(fù)雜度降低到 O(n log n)
- 添加數(shù)據(jù)分頁處理,避免一次處理過多數(shù)據(jù)
- 優(yōu)化后 CPU 使用率降低到正常水平
附錄: 修改執(zhí)行策略(可選)
如果希望不帶 -ExecutionPolicy 參數(shù)即可運(yùn)行,可臨時(shí)提升用戶策略:
# 以管理員身份啟動 PowerShell 后執(zhí)行
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
僅本用戶生效,本地腳本可直接執(zhí)行,網(wǎng)絡(luò)下載腳本需簽名。
附錄: 診斷工具對比
| 工具 | 用途概覽 | 是否實(shí)時(shí) | 輸出類型 | 常見用途 |
| ————– | —————— | —– | ————– | ——————- |
| `dotnet-trace` | 實(shí)時(shí)采樣追蹤(類似 `perf`) | 實(shí)時(shí) | `.nettrace` 文件 | 性能熱點(diǎn)分析、高 CPU、函數(shù)采樣分析 |
| `dotnet-dump` | 獲取并分析進(jìn)程的內(nèi)存轉(zhuǎn)儲(Dump) | 離線 | `.dmp` 文件 | 內(nèi)存泄漏、線程死鎖、對象分析 |
| `dotnet-stack` | 獲取進(jìn)程的線程堆棧信息 | 實(shí)時(shí) | 文本輸出 | 線程狀態(tài)分析、死鎖檢測 |
附錄: 實(shí)際使用場景
| 問題 | 推薦工具 | 說明 |
| ————- | —————- | —————————– |
| 應(yīng)用 CPU 飆高 | `dotnet-trace` | 捕獲方法采樣,查看熱點(diǎn)代碼 |
| 某個(gè)請求特別慢 | `dotnet-trace` | 配合 EventPipe 分析慢操作 |
| 應(yīng)用突然掛住/假死 | `dotnet-dump` | 導(dǎo)出 dump 分析線程死鎖 |
| 內(nèi)存持續(xù)增長,懷疑內(nèi)存泄漏 | `dotnet-dump` | 使用 `dumpheap`, `gcroot` 定位引用鏈 |
| 異常頻繁但日志信息不夠 | `dotnet-trace` | 追蹤異常事件 |
| 離線/無法實(shí)時(shí)調(diào)試 | `dotnet-dump` | 可用于生產(chǎn)環(huán)境導(dǎo)出后離線分析 |
這樣,一套從"獲取 PID → 運(yùn)行腳本 → 查看結(jié)果 → 問題定位 →
匯報(bào)"的流程就完成了,現(xiàn)場同事照流程走即可快速精準(zhǔn)地收集 .NET
運(yùn)行態(tài)信息。