现场 w3wp 卡顿,dump 抓回来托管栈全是死的:一次从 696 万对象里挖根因的排查实录

作者:技术从业16年,踩过坑、做过技术负责人、带过团队,也亲眼看着AI把很多”理所当然”的事情重新洗牌。不追热点,只写真实踩过的坑和总结过的东西,欢迎关注一起交流。

起因:现场说"系统卡得没法用了"

周三下午,现场群里炸了。

一套跑了十几年的 ERP/WMS 系统,基于 ASP.NET WebForms,跑在 IIS 上。业务方反馈:“点哪儿都卡,有时候转半天没反应,有时候突然又好了。”

这种"时好时坏"的卡顿,是排查里最磨人的一类。CPU 打满型好办,性能计数器一盯就知道哪条 SQL、哪个接口在烧;死锁也好办,看日志就能定位。偏偏是间歇性的——你盯着它,它不犯;你一走,它又卡。

现场运维小张还算靠谱,趁卡顿的时候,用任务管理器右键 → 创建转储文件,抓了一个 w3wp.exe 的 dump 发给我。2.46 GB,Full Memory。

我接过来,倒了杯水,觉得这活儿半小时能搞定。

结果我被这个 dump 折磨了整整一下午。 因为它,是废的。

这篇记录的就是这次排查的全过程。如果你也遇到过 ASP.NET 间歇卡顿、也抓过看起来没问题的 dump 却分析不出名堂,照着走,能少走至少半天弯路。我把能踩的坑都踩了。

第一个坎:dump 抓回来了,托管栈全是死的

抓回 dump,第一件事是上 WinDbg 分析。这系统的运行时是 .NET Framework 4.8,所以我得先加载 SOS 扩展,才能看托管堆和托管线程栈。

命令很标准:

0:000> .loadby sos clr
0:000> ~* e !clrstack

.loadby sos clr 的意思是" clr 模块在哪儿,就去那儿找 sos.dll 并加载"。~* e !clrstack 是"对每个线程都执行一次 !clrstack",把所有托管线程的调用栈打出来。

回车,刷屏。

刷出来的却不是栈,是一屏的报错:

OS Thread Id: 0xf21c (0)
Unable to walk the managed stack. The current thread is likely not a 
managed thread. You can run !threads to get a list of managed threads in 
the process
Failed to start stack walk: 80070057

每个线程都是这行:Failed to start stack walk: 80070057

80070057E_INVALIDARG——参数无效。意思是 SOS 想遍历托管栈,但遍历器启动就失败了。所有线程的托管栈,一个都读不出来。

我盯着屏幕,心里咯噔一下。

这是排查最尴尬的局面:dump 抓回来了,原生栈能看到,但托管世界一片漆黑。 而这系统是 .NET 的,业务逻辑全在托管代码里——读不到托管栈,就等于知道案发地点,却不知道凶手是谁、用的什么凶器。

那一刻我怀疑过很多方向:是不是 SOS 版本不对?是不是 dump 损坏了?是不是抓的时候进程状态特殊?

但有一个方向,我没第一时间往那想——是不是 dump 的抓取方式本身就有问题?

这一关给我的教训很朴素,也是这篇文章最想让你记住的一句话:任务管理器抓的 .NET dump,托管栈经常是死的。 这不是玄学,是机制问题。下面我慢慢讲清楚。

小反转:先别慌,看 !analyze -v 说啥

在怀疑一切之前,我习惯先让 WinDbg 自己说一遍——跑 !analyze -v,这是 WinDbg 的自动分析命令,会帮你定位异常、故障模块。

跑完,输出里有一段很显眼:

FAILURE_BUCKET_ID:  BREAKPOINT_80000003_w3wphost.dll!WP_IPM::WaitForShutdown
EXCEPTION_CODE_STR:  80000003
STACK_TEXT:
ntdll!NtWaitForSingleObject+0x14
KERNELBASE!WaitForSingleObjectEx+0x93
w3wphost!WP_IPM::WaitForShutdown+0x11
w3wphost!AppHostInitialize+0x147
w3wp!wmain+0x420

新人看到 EXCEPTION_CODE 80000003FAILURE_BUCKET 这种字眼,很容易慌——“完了,进程崩了,是个断点异常!”

别慌。这是个假警报。

80000003 是 break 指令异常,也就是"断点"。但你看栈——w3wphost!WP_IPM::WaitForShutdown,这是 w3wp 主线程在"等待关闭"的循环里。这不是崩溃,是有人主动打断进程、抓了个 dump。 任务管理器抓 dump 就是这么干的:给进程注入一个断点,趁进程停在断点的瞬间把内存拷出来。

所以 !analyze -v 这里报告的"异常",本质是"抓 dump 这个动作本身",跟卡顿半毛钱关系都没有。

带团队这些年,我特别怕新人遇到 !analyze -v 报异常就紧张。先看栈在哪儿——栈停在 WaitForShutdown,是抓取断点;栈停在某个业务方法里,才是真现场。判断"这是抓取动作还是真故障"的能力,是 dump 分析的第一道分水岭。

但这里也确认了一个坏消息:栈顶是 WaitForShutdown,说明抓 dump 的瞬间,这个 w3wp 主线程是闲着的。 这是个不祥之兆——卡顿如果是持续性的,抓到的瞬间线程应该在忙;抓到闲着的,很可能没抓到现场。

我心里隐隐有了预感,但还得验证。

排查方向一:是不是 SOS 版本不对?

托管栈读不出来,最常见的解释是 SOS 版本和 CLR 版本不匹配。SOS 是 CLR 的配套调试扩展,版本必须严格对上,差一个小版本号都可能出幺。

我让 WinDbg 报了一下版本:

CLR Version: 4.8.4110.0
SOS Version: 4.8.9139.0

果然对不上。CLR 是 4.8.4110,但自动加载的 SOS 是 4.8.9139——我本机装的 .NET 比生产服务器新.loadby 顺着我本机的 .NET 目录加载了更新版的 SOS。

第一反应是找匹配的 SOS。.loadby 加载错了,我手动 .load 指定路径总行吧。我本机有 .NET Framework 自带的 SOS:

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\sos.dll

加载它,重试 !clrstack。结果——还是 80070057,全部失败。

我本机的 Framework64 sos.dll 也是 4.8.9139,版本还是不对。

那加载精确匹配的 4.8.4110 版本。好在 WinDbg 自动从微软符号服务器下载匹配 SOS 时,把它存到了本地符号缓存:

C:\ProgramData\dbg\sym\SOS_AMD64_AMD64_4.8.4110.00.dll\
  ...\SOS_AMD64_AMD64_4.8.4110.00.dll   ← image 4.8.4110.0,精确匹配

我卸掉错的,加载这个精确匹配的:

0:000> .unload sos
0:000> .load C:\ProgramData\dbg\sym\...\SOS_AMD64_AMD64_4.8.4110.00.dll
0:000> .chain      ← 确认加载的是 4.8.4110
0:000> ~* e !clrstack

结果——依然 80070057

到这里,结论已经清晰:这不是 SOS 版本问题。 我换了三个 SOS,版本从 4.8.9139 换到精确匹配的 4.8.4110,托管栈遍历照样失败。

这是我踩的第一个大坑,也是新手最容易卡住的地方。!clrstack 失败,第一反应都是"换 SOS 版本",但换完还不行,就该意识到:不是 SOS 的锅,是这个 dump 本身的托管栈不可走。 别在 SOS 版本上耗太久——换两个还不行,方向就错了。

我还试了 !eestack -ee(混合栈遍历器,号称更稳健),同样失败。这下死心了:这个 dump 的托管栈,彻底读不出来。

那一刻,我反而松了口气。因为方向错了不可怕,可怕的是不知道方向在哪。 现在方向明确了——不是分析能力的问题,是抓取方式的问题。

反转:真正的问题在抓取方式——任务管理器 vs procdump

托管栈为什么读不出来?这要回到 dump 是怎么抓的。

现场小张用的是任务管理器 → 右键进程 → 创建转储文件。这条路最省事,点一下就行,运维人人都会。但它有个致命弱点:它抓的 dump,托管堆的元数据经常不完整,导致 SOS 遍历栈时拿不到有效上下文,于是 80070057

这不是任务管理器的 bug,是它抓取时机和方式的局限——它用最简单的注入断点方式抓,没像专业工具那样做托管堆的状态保全。

正确的姿势是用 procdump(Sysinternals 出品,微软官方)。 procdump 抓 dump 时会确保托管堆状态完整,SOS 能正常遍历。

我让小张用 procdump 重抓。命令其实就一行:

procdump64.exe -ma <w3wp的PID> w3wp.dmp

-ma 是 Full Memory dump,抓全内存。对 .NET 排查必须用 -ma——少了它,托管堆和栈照样读不全。

更进一步,针对间歇卡顿,procdump 还能"挂着等"——CPU 一飙高就自动抓:

# CPU 超过 80% 持续 5 秒,自动抓 1 个
procdump64.exe -ma -c 80 -s 5 <PID> w3wp_cpu.dmp

# 抓 3 个(间歇卡顿要多抓几个对比)
procdump64.exe -ma -c 80 -s 5 -n 3 <PID>
参数含义
-maFull Memory dump(.NET 排查必选)
-c 80CPU 超过 80% 时触发
-s 5持续 5 秒才触发(过滤瞬时毛刺)
-n 3连续抓 3 个

但这里有个现实问题:现场卡顿是间歇的,挂 procdump 等触发,可能等半天也等不到。 而我手头这个已经抓好的、托管栈读不出的 dump,难道就白瞎了?

不白瞎。 我做了个决定:这个 dump 虽然 !clrstack 废了,但 !threads!dumpheap!eeheap 这些命令都正常——它们不依赖栈遍历,只读堆结构。 我可以从"线程状态"和"托管堆对象分布"两个角度,把根因挖出来。

这是我踩坑后想明白的一件事:一个不完美的 dump,不代表没价值。 抓 dump 这事讲究时机,现场不一定每次都抓得准。与其干等重抓,不如先把能榨的信息榨干。分析能力体现在"手里只有半张牌,也能打出结论"。

换个姿势:从"线程在干嘛"入手

托管栈读不了,我换第一个角度——看线程在等什么。 这条路靠原生栈(~* kb),不依赖 SOS,一定走得通。

先看线程池状态。!threadpool 是看线程池"忙不忙"的关键命令:

0:000> !threadpool
CPU utilization: 0%
Worker Thread: Total: 34 Running: 1 Idle: 33 MaxLimit: 32767 MinLimit: 32
Work Request in Queue: 0
Completion Port Thread: Total: 2 Free: 2
Number of Timers: 2

我盯着这五行,心里那个"没抓到现场"的预感坐实了。

逐行拆解:

指标含义
CPU utilization0%抓取瞬间 CPU 几乎空闲
Worker Thread Running1 / 3434 个工作线程,只有 1 个在跑,33 个闲着
Work Request in Queue0线程池待处理队列为空,没有请求积压
Completion Port Free2 / 2IO 完成端口线程全空闲

翻译成人话:抓 dump 的这一刻,系统是闲着的。 CPU 没打满,线程没排队,请求没积压。

这跟"卡得没法用"的反馈,对不上。

但别急——这恰恰印证了"间歇性"。卡顿是一阵一阵的,抓到的这一秒正好是阵歇期。这正常,间歇卡顿的 dump 本来就难抓到峰值。

再看有没有锁竞争。!syncblk 看托管 Monitor 锁:

0:000> !syncblk
Index SyncBlock MonitorHeld Recursion Owning Thread Info  SyncBlock Owner
-------------------------------------------------------------
Total           659
Free            351

竞争表是空的——Total 659 个 SyncBlock,Free 351没有一个线程在等锁。排除了锁竞争和死锁。

再看 CPU 累计消耗。!runaway 列出每个线程的累计 CPU 时间,是找"哪个线程在烧 CPU"的利器:

0:000> !runaway 7
 User Mode Time
  Thread       Time
   34:15d0c     0 days 0:00:02.812
  106:15fdc     0 days 0:00:02.453
   98:ad0       0 days 0:00:01.390
   89:14df0     0 days 0:00:01.359
   ...(后面越来越小)

这里有个坑,新手必栽。 !runaway 默认会输出三段:User Mode Time(用户态)、Kernel Mode Time(内核态)、Elapsed Time(线程存活时长)。

我看到第一段 User Mode Time 最高才 2.8 秒——但这系统已经跑了 20 小时 51 分钟。20 小时累计才烧了 2.8 秒 CPU,等于几乎没在计算。

但如果你看岔了,去看第三段 Elapsed Time,会吓一跳:

Elapsed Time
   3:4ef0     0 days 0:31:31.428      ← 31 分钟?!
  124:f3c     0 days 0:30:31.393

一堆线程"31 分钟"。新人容易误读成"这些线程烧了 31 分钟 CPU"。错。 Elapsed Time 是线程从创建到现在的存活时长,不是 CPU 占用——线程一直在那儿挂着等任务,31 分钟只是它"活了"31 分钟,跟 CPU 一毛钱关系没有。

!runaway 这个命令,只看 User Mode Time 那一段。 Elapsed Time 是存活时长,最唬人——一堆 30 分钟的数字,新手一看以为 CPU 爆了,其实是线程在睡觉。这个误读,我见过不止一个人栽。

到这里,线程角度的结论很清楚了:抓取瞬间系统空闲、无锁竞争、CPU 累计消耗极低。 急性瓶颈(CPU 打满 / 死锁 / 线程池饿死)一个都没坐实。

但"没坐实"不等于"没有"。卡顿是间歇的,抓到闲着的一秒,说明不了问题不存在。只是这条线索断了,得换一条。

排查方向二:统计线程在等什么,揪出等待大户

线程角度的宏观状态看完了,我再钻进去——100 多个线程,到底各自在等什么? 这个统计能透露很多信息。

我用的是"所有线程原生栈"(~* kb),然后对栈顶的等待函数做计数。在 WinDbg 里没法直接统计,我把输出导出来,用脚本 grep + sort + uniq 计数。

栈顶等待函数分布:

NtWaitForSingleObject          202
NtRemoveIoCompletion            70
NtWaitForMultipleObjects        10
NtWaitForWorkViaWorkerFactory    8
NtDelayExecution                 4

200 多个线程都在 NtWaitForSingleObject 上等。但"在等"不等于"被阻塞卡住"——线程池空闲的 worker 也在等(等任务派发)。得看它们等的是哪种东西。

我顺着每个 WaitForSingleObject 往栈上一帧看,统计它的上层调用者:

谁调用了 WaitForSingleObjectEx(取调用点):
    130  clr!CLREventWaitHelper2        ← 托管事件等待
     66  clr!CLRSemaphore::Wait          ← 信号量等待
      2  clr!ThreadpoolMgr::GateThreadStart
      2  w3wphost!WP_IPM::WaitForShutdown

两条主路:130 个线程在 CLREventWaitHelper2(托管事件等待),66 个在 CLRSemaphore::Wait(信号量等待)。

第一眼看到 66 个线程卡在信号量,我心里一动——是不是连接池耗尽?业务请求拿不到 Oracle 连接,全堵在信号量上?这是 .NET 应用卡顿的经典剧本。

但我没急着下结论,把这个信号量等待的线程栈拉出来看:

ntdll!NtWaitForSingleObject+0x14
KERNELBASE!WaitForSingleObjectEx+0x93
clr!CLRSemaphore::Wait+0x7d
clr!ThreadpoolMgr::UnfairSemaphore::Wait+0x115    ← 注意这行
clr!ThreadpoolMgr::WorkerThreadStart+0x2c2

看到 clr!ThreadpoolMgr::UnfairSemaphore::Wait,我心里凉了半截——这不是业务信号量,是线程池自己的派发信号量。 意思是这 66 个线程是空闲的工作线程,在等线程池派活给它们

也就是说,它们不是"被卡住",是"没活干在休息"。

这是我差点栽的第二个坑。看到一堆线程卡在 CLRSemaphore::Wait,第一反应是"连接池/业务信号量耗尽"——但必须往上多看一层。 如果上面是 OracleInternal.ConnectionPool,那是连接池耗尽,真问题;如果上面是 ThreadpoolMgr::UnfairSemaphore,那是空闲 worker 在等任务,正常现象。差一行栈帧,结论天差地别。多看一层栈,是省掉一次误判的关键。

那 130 个 CLREventWaitHelper2 呢?我往上追,上层是 clr!CLREventBase::WaitEx——这是通用的托管事件等待路径,WaitHandle.WaitOne、Task 完成等待、CLR 内部事件都走这里。具体是哪个业务调用,得上托管栈(!clrstack)才能定位——而托管栈恰好读不了。

这条线索,到这里也到头了。线程等待的统计告诉我"没人在干活、也没人被业务锁卡住",但读不到托管栈,就定位不到那 130 个等待事件背后是哪段业务代码。

两条线索都断了,但它们共同指向一个判断:这不是线程在跑业务代码时卡住,更像是一种"全停顿"。 什么样的全停顿能让所有线程同时歇菜、抓 dump 又抓到闲着?

我心里有了一个怀疑的方向。但这次我学乖了,不急着下结论——先把托管堆翻出来看。

排查方向三:翻托管堆,696 万个对象说了实话

线程线索断了,我转向最后一个、也是最可能出真相的地方——托管堆。 !dumpheap!eeheap 这两个命令只读堆结构、不依赖栈遍历,在当前 dump 里完全可用。

先看堆有多大。!eeheap -gc

0:000> !eeheap -gc
Number of GC Heaps: 32
...
GC Heap Size:  Size: 0x2059d508 (542758152) bytes.

542 MB。 一个 w3wp 进程的托管堆 542 MB,活堆,不算小。

但比"多大"更要命的是"堆里都装了啥"。!dumpheap -stat 按类型统计堆上对象,输出按对象大小排序。我把 Top 类型拎出来:

              MT    Count    TotalSize Class Name
00007ffb746559c0  2698813   188546664 System.String
00007ffb1793cb80   846743    54191552 Newtonsoft.Json.Linq.JValue
00007ffb746567d0   159573    43883626 System.Char[]
00007ffb7465aaa0    22899    31977350 System.Byte[]
00007ffb1a0ae6a0   338596    28917744 Newtonsoft.Json.Linq.JToken[]
00007ffb17ea5658   169497    16271712 Newtonsoft.Json.Linq.JArray
...
Total 6967071 objects

最后一行是重锤:Total 6967071 objects——堆上有 696 万个对象。

逐行看,根因自己浮出来了:

类型数量大小
System.String269 万180 MB
Newtonsoft.Json.Linq.JValue84 万54 MB
System.Char[]16 万43 MB
System.Byte[]2.3 万31 MB
Newtonsoft.Json.Linq.JToken[]33 万28 MB
Newtonsoft.Json.Linq.JArray17 万16 MB
Newtonsoft JToken+LineInfo36 万8.6 MB

整个堆,被 Newtonsoft.Json 的 JObject/JArray/JToken 这棵 DOM 树,和 270 万个字符串塞满了。 把 Newtonsoft 相关的全加起来(JValue + JArray + JToken[] + LineInfo + List),光 JSON 的 DOM 对象就占了 ~114 MB,再加上和它关联的 180 MB 字符串——JSON 相关的内存,是这个堆的绝对主体。

那一刻我懂了。

排查卡顿,线程告诉你"现在在干嘛",堆告诉你"过去攒了什么"。 线程抓到闲着,不代表没问题——因为问题可能不在"线程卡住",而在"堆被塞爆,每次 GC 都得停一下"。这两个角度互补,缺一个都可能误判。

根因:不是线程卡住,是 GC 在卡

把所有线索串起来,根因水落石出。

先看大对象堆(LOH)。 !eeheap -gc 会把每代堆分开列,我把 32 个堆的 LOH 段求和:

堆分段大小说明
托管堆总量517.6 MB
大对象堆 LOH148.9 MB≥85KB 大对象,只在 Gen2 回收
小对象堆 SOH368.7 MB主要为长期存活的 Gen2
堆对象总数696 万个

LOH 149 MB,意味着有大量 ≥85KB 的大对象(大 byte[]、大 string、大数组)。这些对象只在 Gen2 回收时才被处理

机制是这样的:

  1. 堆里有 518 MB 活对象、696 万个引用、149 MB LOH;
  2. 其中绝大部分(Gen2 + LOH)是长期存活的——270 万个 String、84 万个 JValue,不会随 Gen0/1 回收消失;
  3. 当堆增长到触发 Gen2 回收(或后台 GC)时,GC 的标记阶段要扫描这 696 万个对象的引用关系
  4. 扫 696 万对象 + 标记 518 MB 堆,需要时间——这段时间里所有托管线程被暂停(STW,Stop-The-World)
  5. 暂停的几十到上百毫秒,用户感知为"卡一下";扫完恢复,又"突然好了"。

这完美解释了所有现象:

  • 为什么时好时坏——GC 不是一直跑,是堆涨到阈值才触发,两次回收之间系统正常;
  • 为什么抓 dump 抓到闲着——STW 停顿通常很短(几十到几百毫秒),抓 dump 的瞬间大概率落在回收之外的正常期;
  • 为什么 CPU 不高——STW 期间主要是 GC 线程在标记扫描,业务线程是"被暂停"而非"在烧 CPU",累计 CPU 自然低;
  • 为什么没有锁竞争——根本不是锁的问题,是 GC 停顿;
  • 为什么堆里全是 JSON 和字符串——这些正是 Gen2 长期存活、撑大堆的元凶。

找根因的时间,远大于改代码的时间。这一下午,我大半时间花在"搞清楚 dump 为什么读不出托管栈、线程在等什么、堆里装了什么",真正定位根因就是 !dumpheap -stat 那一下——看到 270 万 String 和 84 万 JValue 的瞬间,答案就摆在那儿了。但如果没有前面几步把"线程没问题"排除掉,我不会有信心相信"根因在堆"。 排查是排除法,不是猜谜。

验证根因的几个抓手

根因是推断出来的(托管栈读不了,没法 100% 锤实是哪个业务方法在产生这些 JSON),所以我给现场几个验证抓手,确认这个判断:

1. perfmon 看 GC 指标。 这是验证 GC 压力最直接的方式,盯 .NET CLR Memory 这组计数器:

计数器看什么异常信号
% Time in GCGC 占用时间比例卡顿时峰值 > 30% 即坐实
# Gen 2 CollectionsGen2 回收次数卡顿前后是否激增
Large Object Heap SizeLOH 大小是否只增不减
Allocated Bytes/sec每秒分配量是否异常高

2. 抓两份 dump 对比。 隔 10 分钟抓两个(用 procdump!别再用任务管理器),跑 !dumpheap -stat,对比 System.StringJValue 的数量是否在涨。只增不减,就是有地方在持续产生、不释放。

3. 抓一份"卡顿瞬间"的合格 dump。 用 procdump,卡顿当场立即抓:

procdump64.exe -ma <PID> w3wp_stuck.dmp

这份 dump 能跑 !clrstack,就能定位到底是哪个业务方法在产生那 270 万个 String 和 84 万个 JObject。SignalR 推送?API 序列化?Excel 导出?报表生成?还是把结果缓存进 System.Runtime.Caching 后长期不释放?——抓到现场,一锤定音。

复盘:这次排查留下的六条铁律

回看这一下午,把经验浓缩成六条,建议你截图存档:

1. .NET dump,永远用 procdump -ma 抓,别用任务管理器。 任务管理器抓的 dump 托管栈经常是死的(80070057),SOS 遍历不动。procdump 的 -ma 会保全托管堆状态,是 .NET 排查的唯一正解。这一条,值一整个下午。

2. !clrstack 失败,先换一两次 SOS 版本,换两次还不行就换方向。 .loadby sos clr 加载错版本是常事,手动 .load 匹配版(或符号缓存里的 SOS_AMD64_AMD64_4.8.4110.00.dll)。但换两次还 80070057,就别在 SOS 上耗了——是 dump 的问题,不是工具的问题。

3. !runaway 只看 User Mode Time,别被 Elapsed Time 骗了。 Elapsed Time 是线程存活时长,一堆 30 分钟的数字是线程在睡觉,不是在烧 CPU。看岔了,会误判成"CPU 爆了"。

4. 线程等待要往上多看一层栈,别只看栈顶函数名。 CLRSemaphore::Wait 可能是连接池耗尽(业务问题),也可能是 ThreadpoolMgr::UnfairSemaphore(空闲 worker,正常)。差一行栈帧,结论相反。

5. 线程告诉你"现在在干嘛",堆告诉你"过去攒了什么"。 两个角度互补。线程抓到闲着不代表没问题——问题可能不在"卡住",而在"堆被塞爆、GC 频繁停顿"。!dumpheap -stat 是 .NET 排查里被低估的神命令,270 万个 String 直接开口说了实话。

6. 不完美的 dump 也有价值,先榨干再重抓。 手头这个 dump 托管栈废了,但 !threadpool/!syncblk/!dumpheap/!eeheap 全能用。与其干等重抓,不如把能读的全读了。分析能力体现在"只有半张牌也能打出结论"。

命令速查:这次的排查工具箱

把这次用到的命令收个尾,下次直接照搬:

命令干嘛用的关键看什么
!analyze -v自动分析异常栈在 WaitForShutdown = 抓取断点,非崩溃
!threadpool线程池状态Queue 是否积压、Running/Idle 比
!syncblk托管锁竞争有无活动 Monitor 等待
!runaway 7各线程累计 CPU只看 User Mode Time
~* kb全线程原生栈统计栈顶等待函数
.loadby sos clr加载 SOS失败则手动 .load 匹配版
~* e !clrstack全线程托管栈80070057 = dump 抓废了
!eeheap -gcGC 各代堆大小LOH、Gen2 占比
!dumpheap -stat堆对象类型统计Top 类型、对象总数

人到中年,最大的变化是学会了"不急"。带团队也好,找第二曲线也好,都不必非要在某个节点交出满分答卷。每天做一点新的尝试,被年轻人带飞几次,被自己蠢哭几回,这一天就没白过。不油腻的秘诀?保持被打脸的机会,然后笑嘻嘻地爬起来。

关注我,咱们一起晒太阳、赶路。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值