jvm和dotnet程序cpu/内存/线程问题排查方案

jvm

环境准备

  1. 确保目标机器上已安装:

    • PowerShell(Windows PowerShell 5.1 或 PowerShell Core 均可)
    • JDK(含 jstat, jmap, jcmd, jstack 等工具; 建议 JDK 11+)
  2. 将以下脚本文件 java_pid_analysis.ps1
    (本脚本适用于java22,其他版本要按需调整命令)放到某固定目录(如
    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
    ===================
    
    - 时间: $currentTime
    - 目标 PID: $targetPid
    
    "@
    
    # 1. OS 层面 CPU/内存/线程
    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; 线程数: {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 状态: $nmtStatus`n"
        } else {
            $reportContent += "- NativeMemoryTracking 未启用`n"
        }
    } catch {
        $reportContent += "- native_memory: Failed to execute`n"
        Write-Host "jcmd failed: $_" -ForegroundColor Red
    }
    
    # 5. JVM 线程数
    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 += "- 线程总数: " + ($threadCount -replace ".*Total threads:\s*", "") + "`n"
        } else {
            $reportContent += "- 线程总数: Could not determine`n"
        }
    } catch {
        $reportContent += "- 线程总数: 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
    

解锁并允许脚本执行

现场机器常会因 PowerShell 执行策略阻止脚本运行,
推荐使用「方法二」一次性绕过:

脚本需要两个参数:

  • 第一个参数,pid
  • 第二个参数, jdk路径,注意到JAVAHOME路径.
powershell.exe -ExecutionPolicy Bypass -File "D:\Scripts\java_pid_analysis.ps1" 12345 cd-to-java-path
  • 仅对本次执行生效, 不改动机器配置.
  • 若要长期使用, 可参考下面" 附录" 修改执行策略.

执行脚本在控制台的输出可能如下:

PS D:\scada\project\预灌装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

脚本产物

  • threaddump21932.txt jstack的结果

  • jvmanalysis21932.txt 汇总分析,内容如下:

       - 时间: 2025-06-16 14:46
       - 目标 PID: 21932
       - OS CPU(s): 2228s; WorkingSet: 3868.61MB; 线程数: 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分配内存: Could not determine
       - 现成dump见: thread_dump_21932.txt
    

标准诊断流程

按照以下流程, 让现场同事快速上手:

  1. 确认目标 PID

    • 使用任务管理器或命令行:

      Get-Process -Name java | Format-Table Id, ProcessName, CPU, WS, StartTime
      
    • 选定要诊断的 JVM 进程的 PID.

  2. 执行诊断脚本

    • 在 PowerShell 中运行:

      cd D:\Scripts\
      powershell.exe -ExecutionPolicy Bypass -File .\java_pid_analysis.ps1 -pid <目标PID>
      
    • 例如:

      powershell.exe -ExecutionPolicy Bypass -File .\java_pid_analysis.ps1 -pid 9876
      
  3. 查看输出结果

    • OS Metrics: CPU(s), WorkingSet(MB), 线程数.
    • jstat -gcutil: 堆各代使用率及 GC 次数/耗时.
    • jmap -heap: 详细堆分代信息, GC 算法.
    • jcmd VM.nativememory summary: Native 内存分配.
    • jcmd Thread.print (count): JVM 内部线程总数.
    • threaddump_.txt: 完整线程栈(含锁信息), 保存在脚本目录.
  4. 问题定位

    • CPU 过高: 定位到哪个线程占用最高, 可在 thread_dump
      中搜索 nid=0x... 对应十进制线程 ID, 然后结合操作系统工具(如
      Process Explorer)查找.
    • 内存 泄漏: 关注 jmap -heap 中 Old Gen 使用率持续上升,
      jcmd native_memory 中 C 堆本地分配异常.
    • 线程阻塞/死锁: 在 thread_dump 中搜索 BLOCKED,
      WAITING, Deadlock.
  5. 汇报模板 现场诊断完成后, 可用如下模板整理汇报:

    - 时间: 2025-06-16 14:30
    - 目标 PID: 9876
    - OS CPU(s): 120.34s; WorkingSet: 1024MB; 线程数: 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
    - 初步结论: OldGen 使用过高 + 多线程阻塞于 XX 锁, 建议...
    

附录: 修改执行策略(可选)

如果希望不带 -ExecutionPolicy 参数即可运行, 可临时提升用户策略:

# 以管理员身份启动 PowerShell 后执行
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser

仅本用户生效, 本地脚本可直接执行, 网络下载脚本需签名.

这样, 一套从" 获取 PID → 运行脚本 → 查看结果 → 问题定位 → 汇报"
的流程就完成了, 现场同事照流程走即可快速精准地收集 JVM 运行态信息.

.Net

环境准备

  1. 确保目标机器上已安装:

    • PowerShell (Windows PowerShell 5.1 或 PowerShell Core 均可)
    • .NET SDK (包含 dotnet CLI 工具)
    • 诊断工具 (dotnet-trace, dotnet-dump, dotnet-stack 等)
  2. 安装必要的诊断工具:

    # 安装 dotnet-trace 工具
    dotnet tool install --global dotnet-trace
    
    # 安装 dotnet-dump 工具
    dotnet tool install --global dotnet-dump
    
    # 安装 dotnet-stack 工具
    dotnet tool install --global dotnet-stack
    
  3. 将以下脚本文件 dotnet-performance-analysis.ps1 放到某固定目录(如
    D:\Scripts\):

    param(
        [Parameter(Mandatory=$true)]
        [int]$targetPid,
        [string]$outputPath = "."
    )
    
    # 创建输出目录(如果不存在)
    if (-not (Test-Path -Path $outputPath)) {
        New-Item -ItemType Directory -Path $outputPath | Out-Null
    }
    
    # 创建分析报告文件
    $reportFile = Join-Path -Path $outputPath -ChildPath "dotnet_analysis_$targetPid.txt"
    $currentTime = Get-Date -Format "yyyy-MM-dd HH:mm"
    
    Write-Host "==> 检查 .NET 进程 PID=$targetPid" -ForegroundColor Cyan
    
    # 初始化报告内容
    $reportContent = @"
    .NET 进程分析报告
    ===================
    
    - 时间: $currentTime
    - 目标 PID: $targetPid
    
    "@
    
    # 1. OS 层面 CPU/内存/线程
    Write-Host "`n==> OS 指标 (Get-Process)" -ForegroundColor Cyan
    $processInfo = Get-Process -Id $targetPid -ErrorAction SilentlyContinue
    
    if (-not $processInfo) {
        Write-Host "未找到 PID $targetPid 的进程!" -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
    
    # 添加到报告
    $osMetrics = "- OS CPU(s): {0}s; WorkingSet: {1}MB; 专用内存: {2}MB; 虚拟内存: {3}MB; 线程数: {4}; 句柄数: {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"
    
                # 转换为 SpeedScope 格式
                $speedscopeFile = Join-Path -Path $outputPath -ChildPath "trace_$targetPid.speedscope.json"
                Write-Host "`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 工具,无法收集内存转储" -ForegroundColor Yellow
            $reportContent += "- dotnet-dump: 未安装,无法收集内存转储`n"
        } else {
            Write-Host "已安装 $dotnetDumpVersion" -ForegroundColor Green
            $reportContent += "- dotnet-dump: $dotnetDumpVersion`n"
    
            # 收集内存转储
            $dumpFile = Join-Path -Path $outputPath -ChildPath "dump_$targetPid.dmp"
            Write-Host "`n==> 收集内存转储 (Mini 类型)" -ForegroundColor Cyan
            try {
                & dotnet-dump collect --process-id $targetPid --type Mini --output $dumpFile
                $reportContent += "- 内存转储: $dumpFile (使用 'dotnet-dump analyze $dumpFile' 分析)`n"
            } catch {
                $reportContent += "- 内存转储: 收集失败 - $_`n"
                Write-Host "内存转储收集失败: $_" -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
    }
    
    # 保存报告
    $reportContent | Out-File -FilePath $reportFile -Encoding UTF8
    Write-Host "`n==> 分析报告已保存到 $reportFile" -ForegroundColor Green
    
    # 提供分析建议
    Write-Host "`n==> 分析建议:" -ForegroundColor Cyan
    Write-Host "1. 查看 CPU 采样跟踪: 在 https://www.speedscope.app/ 上传 $speedscopeFile 文件" -ForegroundColor White
    Write-Host "2. 分析内存转储: 使用命令 'dotnet-dump analyze $dumpFile'" -ForegroundColor White
    Write-Host "3. 查看堆栈跟踪: 打开 $stackFile 文件" -ForegroundColor White
    

解锁并允许脚本执行

现场机器常会因 PowerShell 执行策略阻止脚本运行,
推荐使用「方法二」一次性绕过:

powershell.exe -ExecutionPolicy Bypass -File "D:\Scripts\dotnet-performance-analysis.ps1" -targetPid 12345
  • 仅对本次执行生效, 不改动机器配置.
  • 若要长期使用, 可参考下面"附录"修改执行策略.

诊断工具详解

dotnet-trace: 性能跟踪与火焰图

dotnet-trace 是一个跨平台的 .NET 性能分析工具,可以收集运行中的 .NET
应用程序的性能数据。

  1. 安装

    dotnet tool install -g dotnet-trace
    
  2. 基本用法

    1. 查看可用进程:

      dotnet-trace ps
      
    2. 收集性能数据:

      # 使用 CPU 采样配置文件收集 5 秒的跟踪
      dotnet-trace collect -p <PID> --profile cpu-sampling --duration 00:00:05
      
    3. 转换为火焰图格式:

      # 将结果转换为火焰图格式
      dotnet-trace convert <trace-file>.nettrace --format speedscope
      
    4. 查看火焰图:

  3. 常用配置文件

    配置文件名称 说明
    `cpu-sampling` 收集 CPU 采样数据,适合分析 CPU 使用率高的问题
    `gc-collect` 收集垃圾回收相关事件
    `gc-verbose` 收集详细的垃圾回收信息
    `http` 收集 HTTP 请求相关事件
    `metrics-browser` 收集浏览器指标

dotnet-dump: 内存转储分析

dotnet-dump 是一个用于收集和分析 .NET
应用程序内存转储的工具,可以帮助诊断内存泄漏、死锁等问题。

  1. 安装

    dotnet tool install -g dotnet-dump
    
  2. 基本用法

    1. 收集内存转储:

      dotnet-dump collect -p <PID>
      
    2. 分析内存转储:

      dotnet-dump analyze <dump-file>.dmp
      
  3. 常用分析命令

    在 dotnet-dump analyze 交互式会话中,可以使用以下命令:

    命令 说明
    `dumpheap` 显示堆上的对象信息
    `gcroot` 查找对象的引用路径
    `threads` 显示所有线程信息
    `clrstack` 显示托管代码的堆栈跟踪
    `dso` 显示当前堆栈边界内的所有托管对象
    `dumpobj` 显示对象的详细信息
    `dumparray` 显示数组的详细信息
    `dumpasync` 显示异步状态机信息
    `exit` 退出分析会话

dotnet-stack: 堆栈跟踪

dotnet-stack 是一个用于收集 .NET
应用程序线程堆栈跟踪的工具,可以帮助诊断线程阻塞、死锁等问题。

  1. 安装

    dotnet tool install -g dotnet-stack
    
  2. 基本用法

    1. 查看可用进程:

      dotnet-stack ps
      
    2. 收集堆栈跟踪:

      dotnet-stack report -p <PID>
      
    3. 收集到文件:

      dotnet-stack report -p <PID> > stack.txt
      

标准诊断流程

按照以下流程,让现场同事快速上手:

  1. 确认目标 PID

    • 使用任务管理器或命令行:

      Get-Process -Name *dotnet* | Format-Table Id, ProcessName, CPU, WS, StartTime
      
    • 选定要诊断的 .NET 进程的 PID。

  2. 执行诊断脚本

    • 在 PowerShell 中运行:

      cd D:\Scripts\
      powershell.exe -ExecutionPolicy Bypass -File .\dotnet-performance-analysis.ps1 -targetPid <目标PID>
      
    • 例如:

      powershell.exe -ExecutionPolicy Bypass -File .\dotnet-performance-analysis.ps1 -targetPid 9876
      
  3. 查看输出结果

    • OS Metrics: CPU(s), WorkingSet(MB), 专用内存(MB),
      虚拟内存(MB), 线程数, 句柄数。
    • dotnet-trace: CPU 采样跟踪文件和 SpeedScope 文件。
    • dotnet-dump: 内存转储文件。
    • dotnet-stack: 堆栈跟踪文件。
  4. 问题定位

    • CPU 过高:使用 SpeedScope 查看火焰图,定位热点方法。
    • 内存 泄漏:使用 dotnet-dump analyze
      分析内存转储,查找大对象和引用路径。
    • 若 *线程阻塞/死锁*:在堆栈跟踪中查找 BLOCKED 或 WAITING
      状态的线程。
  5. 汇报模板 现场诊断完成后,可用如下模板整理汇报:

    - 时间: 2025-06-16 14:30
    - 目标 PID: 9876
    - OS CPU(s): 120.34s; WorkingSet: 1024MB; 专用内存: 800MB; 虚拟内存: 2048MB; 线程数: 45; 句柄数: 1200
    - CPU 热点: 方法 A (30%), 方法 B (15%), 方法 C (10%)
    - 内存分析: 发现类型 X 的实例过多 (10000+),可能存在内存泄漏
    - 线程状态: 3 个线程阻塞在 Y 锁上,可能存在死锁
    - 初步结论: 内存使用过高 + 线程阻塞,建议优化 X 类型的对象生命周期管理
    

常见问题分析与解决方案

CPU 使用率过高

  1. 症状

    • 应用响应缓慢
    • 任务管理器显示 CPU 使用率高
    • 服务器负载高
  2. 分析方法

    1. 使用 dotnet-trace 收集 CPU 采样数据:

      dotnet-trace collect -p <PID> --profile cpu-sampling
      
    2. 转换为 SpeedScope 格式并在 https://www.speedscope.app/ 查看

    3. 在火焰图中寻找占用 CPU 时间最多的方法(最宽的部分)

  3. 常见原因与解决方案

    • *无限循环或低效算法*:优化算法,避免不必要的循环
    • *过度分配/回收对象*:使用对象池,减少临时对象创建
    • *不必要的字符串操作*:使用 StringBuilder,避免频繁字符串拼接
    • *线程争用*:减少锁的粒度,使用更高效的并发模式

内存泄漏

  1. 症状

    • 应用内存使用随时间持续增长
    • 频繁的垃圾回收
    • 最终导致 OutOfMemoryException
  2. 分析方法

    1. 使用 dotnet-dump 收集内存转储:

      dotnet-dump collect -p <PID>
      
    2. 分析内存转储:

      dotnet-dump analyze <dump-file>.dmp
      
    3. 使用以下命令查找大对象:

      dumpheap -stat    # 按类型统计对象
      dumpheap -type <TypeName>  # 查看特定类型的所有对象
      gcroot <address>  # 查找对象的引用路径
      
  3. 常见原因与解决方案

    • *事件订阅未取消*:确保取消事件订阅,使用弱引用事件模式
    • *静态集合持有对象*:避免在静态集合中存储长生命周期对象
    • *未释放的资源*:正确实现 IDisposable 模式,使用 using 语句
    • *缓存未设置上限*:使用 MemoryCache 并设置适当的过期策略

线程死锁

  1. 症状

    • 应用挂起或无响应
    • 某些操作永远不会完成
    • 线程数持续增长
  2. 分析方法

    1. 使用 dotnet-stack 收集堆栈跟踪:

      dotnet-stack report -p <PID> > stack.txt
      
    2. 在堆栈跟踪中查找 BLOCKED 或 WAITING 状态的线程

    3. 分析线程之间的依赖关系,寻找循环依赖

  3. 常见原因与解决方案

    • *嵌套锁*:避免在持有一个锁的同时获取另一个锁
    • *不一致的锁顺序*:确保始终以相同的顺序获取多个锁
    • *同步上下文死锁*:使用 ConfigureAwait(false)
      避免上下文切换死锁
    • *资源争用*:使用更细粒度的锁,或考虑无锁数据结构

实际案例分析

案例一:线程数不断增长

  1. 问题描述

    应用程序运行一段时间后,线程数不断增长,最终导致系统资源耗尽。

  2. 分析过程

    1. 使用 dotnet-stack 收集堆栈跟踪
    2. 发现大量线程处于等待状态,且都在相似的代码位置
    3. 检查日志发现在数据采集部分和触发数据积累的地方都打印了大量日志
  3. 解决方案

    1. 删除采集部分的冗余日志
    2. 优化日志级别,减少不必要的日志输出
    3. 修改后线程数不再随时间增长,系统能正常归档数据

案例二:CPU 使用率突然飙升

  1. 问题描述

    生产环境中的应用在特定操作后 CPU 使用率突然飙升到 100%。

  2. 分析过程

    1. 使用 dotnet-trace 收集 CPU 采样数据并生成火焰图
    2. 在火焰图中发现某个数据处理方法占用了大量 CPU 时间
    3. 代码审查发现该方法中存在 O(n²) 复杂度的算法
  3. 解决方案

    1. 重构算法,将复杂度降低到 O(n log n)
    2. 添加数据分页处理,避免一次处理过多数据
    3. 优化后 CPU 使用率降低到正常水平

附录: 修改执行策略(可选)

如果希望不带 -ExecutionPolicy 参数即可运行,可临时提升用户策略:

# 以管理员身份启动 PowerShell 后执行
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser

仅本用户生效,本地脚本可直接执行,网络下载脚本需签名。

附录: 诊断工具对比

工具 用途概览 是否实时 输出类型 常见用途
————– —————— —– ————– ——————-
`dotnet-trace` 实时采样追踪(类似 `perf`) 实时 `.nettrace` 文件 性能热点分析、高 CPU、函数采样分析
`dotnet-dump` 获取并分析进程的内存转储(Dump) 离线 `.dmp` 文件 内存泄漏、线程死锁、对象分析
`dotnet-stack` 获取进程的线程堆栈信息 实时 文本输出 线程状态分析、死锁检测

附录: 实际使用场景

问题 推荐工具 说明
————- —————- —————————–
应用 CPU 飙高 `dotnet-trace` 捕获方法采样,查看热点代码
某个请求特别慢 `dotnet-trace` 配合 EventPipe 分析慢操作
应用突然挂住/假死 `dotnet-dump` 导出 dump 分析线程死锁
内存持续增长,怀疑内存泄漏 `dotnet-dump` 使用 `dumpheap`, `gcroot` 定位引用链
异常频繁但日志信息不够 `dotnet-trace` 追踪异常事件
离线/无法实时调试 `dotnet-dump` 可用于生产环境导出后离线分析

这样,一套从"获取 PID → 运行脚本 → 查看结果 → 问题定位 →
汇报"的流程就完成了,现场同事照流程走即可快速精准地收集 .NET
运行态信息。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容