惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
U
Unit 42
GbyAI
GbyAI
M
MIT News - Artificial intelligence
美团技术团队
罗磊的独立博客
雷峰网
雷峰网
量子位
博客园 - 【当耐特】
Last Week in AI
Last Week in AI
D
Docker
小众软件
小众软件
S
SegmentFault 最新的问题
Blog — PlanetScale
Blog — PlanetScale
阮一峰的网络日志
阮一峰的网络日志
宝玉的分享
宝玉的分享
T
Tailwind CSS Blog
WordPress大学
WordPress大学
V
V2EX
博客园_首页
腾讯CDC
The Cloudflare Blog
A
About on SuperTechFans
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC

博客园 - 一线码农

记一次 .NET 某智慧医保云服务Linux 非托管泄露分析 .NET 高级调试技术:超越基础 Dump 分析 .NET 生产环境调试实战指南 记一次 .NET 某光电成像后端解析系统 内存暴涨分析 记一次 .NET 某电力后台监控系统 内存暴涨分析 记一次 .NET 某注塑模具系统 CPU爆高分析 记一次 .NET 某集群管理软件 内存暴涨分析 记一次 .NET 某智慧工厂视觉程序 崩溃分析 记一次 .NET 某低代码开发框架 内存暴涨分析 记一次 .NET 某MES上位机拍照系统 内存暴涨分析 记一次 .NET 某RFID标签打印客户端 崩溃分析 对 .NET FileSystemWatcher引发内存碎片化的 反思 DotMemory系列:5. 如何实现自动化抓取和应用自托管 DotMemory系列:4. 如何分析进程的转储文件 DotMemory系列:3. 堆碎片化引发的内存暴涨分析 DotMemory系列:2. 事件泄露引发的内存暴涨分析 DotMemory系列:1. 终结队列积压引发的内存暴涨分析 记一次 .NET 某医联体管理系统 崩溃分析 记一次 .NET 某光放测试系统 崩溃分析 记一次 .NET 某药品缺陷高速检测系统 卡慢分析 聊一聊 .NET超高内存故障分析方法 的反思 记一次 .NET 某企业ECM内容管理系统 内存暴涨分析 记一次 .NET 某跨境物流系统 内存暴涨分析 记一次 .NET 某中医药附属医院门诊系统 崩溃分析 聊一聊 .NET 中的 CompositeChangeToken 聊一聊 .NET 中的 CancellationTokenSource 记一次 .NET 某CRM物流行业管理系统 崩溃分析
记一次 .NET 某理财管理客户端 OOM溢出分析
一线码农 · 2025-11-13 · via 博客园 - 一线码农

一:背景

1. 讲故事

这是训练营里的学员找到我的,让我帮忙看下为什么他的客户程序会偶发的出现 报错弹框,由于dump比较敏感,这里就不截图发出来了,由于是错误弹框,并不会出现程序崩溃,而且朋友在日志中也看到了 OOM 异常,就是因为这个 OOM 异常导致了后续流程的 报错弹框,说这个程序的内存还行,在业务代码中用了 try catch 吞掉异常了,让我帮忙看下。

由于 OOM dump没到手,而且代码中使用 try catch 吞掉了,有些人可能就没撤了,其实知道 异常两阶段 的朋友应该知道,我们可以在 first chance 的时候抓dump,即 catch 之前,所以就有了下面的捕获脚本。


procdump 20860 -e 1 -f PAVException -ma -o D:\testdump\

顺利拿到dump之后,接下来就是一顿分析了。

二:OOM分析

1. 为什么会 OOM

双击 dump 之后,映入眼帘的就是异常线程的现场信息,参考如下:


This dump file has an exception of interest stored in it.
The stored exception information can be accessed via .ecxr.
(15fc.4fe8): C++ EH exception - code e06d7363 (first/second chance not available)
For analysis of this file, run !analyze -v
eax=2b1aefa0 ebx=19930520 ecx=00000003 edx=00000000 esi=037eebc0 edi=530bb548
eip=77383874 esp=2b1aefa0 ebp=2b1aeffc iopl=0         nv up ei pl nz ac pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000216
KERNELBASE!RaiseException+0x64:
77383874 8b4c2454        mov     ecx,dword ptr [esp+54h] ss:002b:2b1aeff4=224e4fd8

从卦中可以看到 RaiseException 就是托管异常的明证,接下来用 .ecxr ; k 观察异常调用栈。


0:052> .ecxr;k 
eax=2b1aefa0 ebx=19930520 ecx=00000003 edx=00000000 esi=037eebc0 edi=530bb548
eip=77383874 esp=2b1aefa0 ebp=2b1aeffc iopl=0         nv up ei pl nz ac pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000216
KERNELBASE!RaiseException+0x64:
77383874 8b4c2454        mov     ecx,dword ptr [esp+54h] ss:002b:2b1aeff4=224e4fd8
  *** Stack trace for last set context - .thread/.cxr resets it
 # ChildEBP RetAddr      
00 2b1aeffc 52e3c8fb     KERNELBASE!RaiseException+0x64
01 2b1af02c 52fee8fc     coreclr!_CxxThrowException+0x66 [d:\a01\_work\11\s\src\vctools\crt\vcruntime\src\eh\throw.cpp @ 74] 
02 2b1af040 52d481a8     coreclr!ThrowOutOfMemory+0x24 [D:\a\_work\1\s\src\coreclr\src\utilcode\ex.cpp @ 1044] 
03 2b1af074 30b8f91e     coreclr!LargeHeapHandleTable::AllocateHandles [D:\a\_work\1\s\src\coreclr\src\vm\appdomain.cpp @ 381] 
WARNING: Frame IP not in any known module. Following frames may be wrong.
04 2b1af074 05990114     0x30b8f91e
05 2b1af074 52d452e7     0x5990114
06 2b1af0c8 52d453e7     coreclr!AllocateSzArray+0x227 [D:\a\_work\1\s\src\coreclr\src\vm\gchelpers.cpp @ 427] 
07 2b1af14c 5257296e     coreclr!JIT_NewArr1+0xb7 [D:\a\_work\1\s\src\coreclr\src\vm\jithelpers.cpp @ 2723] 
08 2b1af160 52581bcf     System_Private_CoreLib!System.Text.Encoding.GetBytes+0x22 [_/src/libraries/System.Private.CoreLib/src/System/Text/Encoding.cs @ 667] 
09 2b1af168 263e7ad6     System_Private_CoreLib!System.Text.UTF8Encoding.UTF8EncodingSealed.GetBytes+0x1b
0a 2b1af1a8 263e7a43     xxx!xxx.xxxxHashData+0x46

从卦中可以清晰的看到,原来是在 xxxxHashData 中执行了 GetBytes 时抛出的 OOM 异常, 那为什么 GetBytes 会抛出异常呢?这个只能结合源代码说话了。

2. GetBytes 为什么会抛出 OOM

找到 xxxxHashData 下的 GetBytes 方法,截图如下:

从卦中可以看到参数是一个 string,看样子这就是突破口了,使用 !clrstack -a 观察这个 s 的具体值,参考如下:


0:052> !clrstack -a
OS Thread Id: 0x4fe8 (52)
Child SP       IP Call Site
2B1AF0E8 77383874 [HelperMethodFrame: 2b1af0e8] 
2B1AF154 5257296e System.Text.Encoding.GetBytes(System.String) [/_/src/libraries/System.Private.CoreLib/src/System/Text/Encoding.cs @ 667]
    PARAMETERS:
        this (<CLR reg>) = 0x05b5b674
        s (<CLR reg>) = 0x348d1010
    LOCALS:
        <no data>
        <no data>
        <no data>

0:052> !DumpObj /d 348d1010
Name:        System.String
MethodTable: 0568ec98
EEClass:     0569a8c0
Size:        83886094(0x500000e) bytes
String:      <String is invalid or too large to print>
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
056873b4  4000212        4         System.Int32  1 instance 41943040 _stringLength
056854e0  4000213        8          System.Char  1 instance       54 _firstChar
0568ec98  4000211       60        System.String  0   static 05b512b0 Empty

0:052> ? 0x500000e
Evaluate expression: 83886094 = 0500000e

从卦中看真的是吓一跳,string.length=4194w 真尼玛大,并且 string 的重量高达 83M,就是由于这个 83M 的string,被 clr 直接给屏掉了。。。接下来的问题是为什么 clr 会屏掉呢?

3. clr 为什么会屏掉

有一些 clr 基础知识的朋友应该知道,这种 OOM 异常一般是两种情况。

  1. 通过 if 语句判断是否超限,这个在训练营里面都有讲到,参考代码如下:

    // Limit the maximum string size to <2GB to mitigate risk of security issues caused by 32-bit integer
    // overflows in buffer size calculations.
    if (cchStringLength > CORINFO_String_MaxLength)
        ThrowOutOfMemory();

  1. 向托管堆要指定大小的内存要不到的时候,这个可以用 !ao 命令观察。

0:052> !ao
Didn't have enough memory to allocate an LOH segment
Details: LOH Failed to reserve memory 50,331,648 bytes

从上面的卦数据来看,是 clr 向大对象堆预定50M的连续地址空间时,结果要不到,clr非常无奈抛出了这个OOM异常。

接下来的问题是为什么要不到呢?

4. 为什么托管堆拒绝了

有经验的朋友应该知道是咋回事了,对,就是虚拟地址空间不足导致的。。。 可以用 !address -summary 观察虚拟地址空间大小。


0:052> !address -summary

--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
<unknown>                              1091          3e78b000 ( 999.543 MB)  64.47%   48.81%
Free                                    380          1f183000 ( 497.512 MB)           24.29%
Image                                  1039          17d37000 ( 381.215 MB)  24.59%   18.61%
Stack                                   219           6100000 (  97.000 MB)   6.26%    4.74%
Heap                                     38           4751000 (  71.316 MB)   4.60%    3.48%
TEB                                      73            11a000 (   1.102 MB)   0.07%    0.05%
Other                                    21             3d000 ( 244.000 kB)   0.02%    0.01%
PEB                                       1              3000 (  12.000 kB)   0.00%    0.00%

--- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_PRIVATE                            1010          36608000 ( 870.031 MB)  56.12%   42.48%
MEM_IMAGE                              1142          17e6c000 ( 382.422 MB)  24.67%   18.67%
MEM_MAPPED                              330          129f9000 ( 297.973 MB)  19.22%   14.55%

--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_COMMIT                             1937          4fdd5000 (   1.248 GB)  82.42%   62.40%
MEM_FREE                                380          1f183000 ( 497.512 MB)           24.29%
MEM_RESERVE                             545          11098000 ( 272.594 MB)  17.58%   13.31%

从卦中可以看到虽然 MEM_RESERVE=272M ,但没有哪一块是大于 50M 的,所以直接导致灾难的发生,到这里该如何解决呢?这其实也是一个经典的问题,即 32bit 程序 2G 地址空间问题,修改办法如下:

  1. 使用大地址 LargeAddress,让程序尽量吃 4G 内存。
  2. 将程序调整到 64bit,让虚拟地址不再捉襟见肘。

三:总结

这个故障也不算是什么大问题,就像网络时好时坏一样,不过像这种强势部门投放过来的抱怨也是亚历山大的。。。

图片名称