- A+
一:背景
1. 讲故事
前几天有位朋友加wx求助说他的程序最近总是出现内存溢出,很崩溃,如下图:
和这位朋友聊下来,发现他也是搞医疗的,哈哈,.NET 在医疗方面还是很有市场的???,不过对于内存方面出的问题,我得先祈祷一下千万不要是非托管。。。
废话不多说,上 windbg,看能不能先救个急。
二: windbg 分析
1. 找出异常对象
如果内存溢出了,大家应该知道 C# 会抛一个 OutOfMemoryException
异常,而且还会附加到那个执行线程上,所以先用 !t
命令调出当前的所有托管线程。
0:000> !t ThreadCount: 17 UnstartedThread: 0 BackgroundThread: 12 PendingThread: 0 DeadThread: 4 Hosted Runtime: no Lock ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception 0 1 16b0 007da908 26020 Preemptive 64EDD188:00000000 00823830 1 STA System.OutOfMemoryException 57b53d90 2 2 af8 007e9dc8 2b220 Preemptive 00000000:00000000 007d4838 0 MTA (Finalizer) 3 3 1d94 0081af28 21220 Preemptive 00000000:00000000 007d4838 0 Ukn 5 6 246c 0772b960 102a220 Preemptive 00000000:00000000 007d4838 0 MTA (Threadpool Worker) 8 47 277c 2eebf038 8029220 Preemptive 00000000:00000000 007d4838 0 MTA (Threadpool Completion Port) XXXX 41 0 2eebf580 1039820 Preemptive 00000000:00000000 007d4838 0 Ukn (Threadpool Worker)
可以清楚的看到,0号
线程果然带了一个 System.OutOfMemoryException
,接下来用 !pe
查查这个异常的调用栈信息。
0:000> !pe 57b53d90 Exception object: 57b53d90 Exception type: System.OutOfMemoryException Message: 没有足够的内存继续执行程序。 InnerException: <none> StackTrace (generated): SP IP Function 00482C80 6450BD46 mscorlib_ni!System.Runtime.InteropServices.Marshal.AllocHGlobal(IntPtr)+0xc2fdf6 00482CB0 198DCEF2 UNKNOWN!FastReport.Export.TTF.TrueTypeCollection..ctor(System.Drawing.Font)+0xe2 00482D00 198DCC0F UNKNOWN!FastReport.Export.TTF.ExportTTFFont.GetFontData()+0x47 00482D58 198DAD54 UNKNOWN!FastReport.Export.Pdf.PDFExport.WriteFont(FastReport.Export.TTF.ExportTTFFont)+0xa4 00483A7C 198D9CD5 UNKNOWN!FastReport.Export.Pdf.PDFExport.AddPDFFooter()+0x8d 00483C38 198D9B53 UNKNOWN!FastReport.Export.Pdf.PDFExport.Finish()+0x23 00483C80 19938119 UNKNOWN!FastReport.Export.ExportBase.Export(FastReport.Report, System.IO.Stream)+0x229 00483CD8 19937A9D UNKNOWN!FastReport.Export.ExportBase.Export(FastReport.Report, System.String)+0x4d 00483D08 19937A3D UNKNOWN!FastReport.Report.Export(FastReport.Export.ExportBase, System.String)+0xd 00483D10 15D9FA39 UNKNOWN!xxxx.xxx.FormPrint.PrintPdf(Boolean, System.String, xxxx.DAL.xxx.DataObject.IPatinfoBase, Boolean, System.String)+0x359 00483DF0 137B265A UNKNOWN!xxxx.UI.xxx.PrintOrdert2PDF.Handle(System.Object[])+0x3ca 00483EB4 1178B36C xxx_PrintOrder2Pdf!xxxx.xxx.PrintOrder2Pdf.Form1.timer1_Tick(System.Object, System.EventArgs)+0xca4 0048414C 117884DD UNKNOWN!System.Windows.Forms.Timer.OnTick(System.EventArgs)+0x15 00484154 117883A0 UNKNOWN!System.Windows.Forms.Timer+TimerNativeWindow.WndProc(System.Windows.Forms.Message ByRef)+0x38 00484160 07C939B7 UNKNOWN!System.Windows.Forms.NativeWindow.Callback(IntPtr, Int32, IntPtr, IntPtr)+0x5f
从上面的调用栈可以看出,貌似程序是在做一个 pdf 打印,最后在 Marshal.AllocHGlobal
上抛了异常,熟悉这个方法的朋友应该知道,它就是用来分配 非托管内存
的。。。 情况貌似有点不妙。???
接下来用 ILSpy
查一下 AllocHGlobal 方法的源码,看看有什么可挖掘的地方。
从图中源码逻辑可以看出,一旦非托管内存分配失败,托管层上手工抛出 OutOfMemoryException
异常,我去,这难道是非托管内存溢出啦???
2. 真的是非托管溢出了吗?
要鉴别是否为非托管堆出的问题,还是用那个老办法,看看 MEM_COMMIT Size ≈ GC Heap Size
即可。
- 用
!address -summary
查看进程的内存使用量
0:000> !address -summary --- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal <unknown> 16334 460bb000 ( 1.094 GB) 78.00% 54.72% Free 11177 26319000 ( 611.098 MB) 29.84% Image 831 e48e000 ( 228.555 MB) 15.91% 11.16% Heap 184 4547000 ( 69.277 MB) 4.82% 3.38% Stack 61 11c0000 ( 17.750 MB) 1.24% 0.87% Other 10 60000 ( 384.000 kB) 0.03% 0.02% TEB 20 24000 ( 144.000 kB) 0.01% 0.01% PEB 1 3000 ( 12.000 kB) 0.00% 0.00% --- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal MEM_COMMIT 16213 521bd000 ( 1.283 GB) 91.43% 64.15% MEM_FREE 11177 26319000 ( 611.098 MB) 29.84% MEM_RESERVE 1228 7b1a000 ( 123.102 MB) 8.57% 6.01%
从上面的 MEM_COMMIT
指标可以看出内存使用量为 1.28 G
。
- 用
!gcheap -gc
看看托管堆的大小
0:000> !eeheap -gc Number of GC Heaps: 1 generation 0 starts at 0x64c534f8 generation 1 starts at 0x64bccb84 generation 2 starts at 0x02531000 ephemeral segment allocation context: none GC Heap Size: Size: 0x195be7b0 (425453488) bytes.
从最后一行可以看出托管堆占用了 425453488/1024/1024 = 405M
。
也就是说大概 800M 不知道哪里去了,看似有点吓人,其实算算也还可以,这里我稍微补充一下,看下面的公式:
MEM_COMMIT (1.28G) = Image (228M) + Heap (69M) + Stack (18M) + GCHeap(450M) + GCLoader (153M) + else = 918M
从上面列出来的信息可以看出,最后累积出的 918M
和 内存使用量 1.28G
差不了多少,有些朋友可能要问, 这个 GCLoader
怎么算出来的,很简单,它是 CLR 的加载堆,使用 !eeheap -loader
即可。
0:000> !eeheap -loader -------------------------------------- Total LoaderHeap size: Size: 0x995a000 (160800768) bytes total, 0x13e000 (1302528) bytes wasted. =======================================
到这里,我陷入了僵局???,才 1.28G
的内存占用,怎么就会把程序给弄溢出了? 既然内存上看不出问题,那就从线程上入手吧,看看他们都在做什么?
3. 查看每个线程都在做什么?
要想看线程,可以用 ~*e !clrstack
调出所有线程的托管栈,突然我发现主线程有点奇怪,调用栈特别深,不信我截图跟你看。
从图中可以看到,xxx.xxx.PrintOrder2Pdf.Form1.timer1_Tick
高达 133 个,这说明 Form 窗体上有一个 timer 没有控制好,出现重复执行的情况了,不管怎么说,这个地方肯定有问题,接下来要做的就是把这个 timer1_Tick
源码导出来看看怎么写的,还是用那个 !name2ee + !savemodule
老命令导出,代码简化如下。
private void timer1_Tick(object sender, EventArgs e) { if (!IsContinue) { PrintMsg("等待上一扫描执行完毕"); IsContinue = true; return; } IsContinue = false; GetPatList(); if (PatList == null || PatList.Rows.Count == 0) { timer1.Interval = 600000; PrintMsg("xxxx"); IsContinue = true; return; } for (int i = 0; i < PatList.Rows.Count; i++) { xxx } IsContinue=true; }
从代码中可以看出,这个方法用了很多的 IsContinue
来踢掉重复请求,但最终还是出了bug,导致无限量递归,跟朋友沟通后建议用 Stop()
和 Start()
来处理,参考如下代码:
private void button1_Click(object sender, EventArgs e) { timer1.Interval = 2000; timer1.Tick += Timer1_Tick; timer1.Start(); } private void Timer1_Tick(object sender, EventArgs e) { timer1.Stop(); MessageBox.Show("hello"); timer1.Start(); }
起码这种 停止
再 启动
的方式肯定能规避timer的重复执行,先把这个改了再说,给医院那边先部署上,再观后效。。。
三:总结
朋友在五一节后,也就是前天给医院部署上了,昨天反馈没有再出现问题,截一张图证明一下???。
大家应该也看的出来,其实我心里是没底的。。。后续和朋友再沟通,发现了三点信息:
-
医生的电脑配置为 8G or 12G
-
有时候为了一些便利,医生会开双进程
-
还有更多其他模块的内存溢出案例
看了下程序是采用插件式编程,而且还用了 DevExpress + FastReport
这些重量级的组件,再搭配上医生开的双进程让电脑余下的贫瘠内存更加吃紧,可能这才是程序在 1.2G
就分配不到非托管内存的深层原因,现场情况应该更复杂,只能先到这里了。
建议措施如下,很简单。
- 增加电脑的配置,up 到 16G 最好了,毕竟甲方都不差钱 ???
更多高质量干货:参见我的 GitHub: dotnetfly