记一次 .NET 医院CIS系统 内存溢出分析
一:背景
1. 講故事
前幾天有位朋友加wx求助說他的程序最近總是出現內存溢出,很崩潰,如下圖:
和這位朋友聊下來,發現他也是搞醫療的,哈哈,.NET 在醫療方面還是很有市場的????????????,不過對于內存方面出的問題,我得先祈禱一下千萬不要是非托管。。。
廢話不多說,上 windbg,看能不能先救個急。
二:windbg 分析
1. 找出異常對象
如果內存溢出了,大家應該知道 C# 會拋一個 OutOfMemoryException 異常,而且還會附加到那個執行線程上,所以先用 !t 命令調出當前的所有托管線程。
0:000>?!t ThreadCount:??????17 UnstartedThread:??0 BackgroundThread:?12 PendingThread:????0 DeadThread:???????4 Hosted?Runtime:???noLock??ID?OSID?ThreadOBJ????State?GC?Mode?????GC?Alloc?Context??Domain???Count?Apt?Exception0????1?16b0?007da908?????26020?Preemptive??64EDD188:00000000?00823830?1?????STA?System.OutOfMemoryException?57b53d902????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???????Function00482C80?6450BD46?mscorlib_ni!System.Runtime.InteropServices.Marshal.AllocHGlobal(IntPtr)+0xc2fdf600482CB0?198DCEF2?UNKNOWN!FastReport.Export.TTF.TrueTypeCollection..ctor(System.Drawing.Font)+0xe200482D00?198DCC0F?UNKNOWN!FastReport.Export.TTF.ExportTTFFont.GetFontData()+0x4700482D58?198DAD54?UNKNOWN!FastReport.Export.Pdf.PDFExport.WriteFont(FastReport.Export.TTF.ExportTTFFont)+0xa400483A7C?198D9CD5?UNKNOWN!FastReport.Export.Pdf.PDFExport.AddPDFFooter()+0x8d00483C38?198D9B53?UNKNOWN!FastReport.Export.Pdf.PDFExport.Finish()+0x2300483C80?19938119?UNKNOWN!FastReport.Export.ExportBase.Export(FastReport.Report,?System.IO.Stream)+0x22900483CD8?19937A9D?UNKNOWN!FastReport.Export.ExportBase.Export(FastReport.Report,?System.String)+0x4d00483D08?19937A3D?UNKNOWN!FastReport.Report.Export(FastReport.Export.ExportBase,?System.String)+0xd00483D10?15D9FA39?UNKNOWN!xxxx.xxx.FormPrint.PrintPdf(Boolean,?System.String,?xxxx.DAL.xxx.DataObject.IPatinfoBase,?Boolean,?System.String)+0x35900483DF0?137B265A?UNKNOWN!xxxx.UI.xxx.PrintOrdert2PDF.Handle(System.Object[])+0x3ca00483EB4?1178B36C?xxx_PrintOrder2Pdf!xxxx.xxx.PrintOrder2Pdf.Form1.timer1_Tick(System.Object,?System.EventArgs)+0xca40048414C?117884DD?UNKNOWN!System.Windows.Forms.Timer.OnTick(System.EventArgs)+0x1500484154?117883A0?UNKNOWN!System.Windows.Forms.Timer+TimerNativeWindow.WndProc(System.Windows.Forms.Message?ByRef)+0x3800484160?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 查看進程的內存使用量
從上面的 MEM_COMMIT 指標可以看出內存使用量為 1.28 G。
用 !gcheap -gc 看看托管堆的大小
從最后一行可以看出托管堆占用了 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 最好了,畢竟甲方都不差錢 ????????????
總結
以上是生活随笔為你收集整理的记一次 .NET 医院CIS系统 内存溢出分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C# this关键字的3种用法
- 下一篇: 动手实现一个适用于.NET Core 的