记一次 .NET 医院CIS系统 内存溢出分析

共 13826字,需浏览 28分钟

 ·

2021-05-16 10:58

一:背景

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 16b007da908     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 2460772b960   102a220 Preemptive  00000000:00000000 007d4838 0     MTA (Threadpool Worker) 
   8   47 2772eebf038   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 object57b53d90
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
    0048414117884DD 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 最好了,毕竟甲方都不差钱 😂😂😂







回复 【关闭】广
回复 【实战】获取20套实战源码
回复 【被删】
回复 【访客】访
回复 【小程序】学获取15套【入门+实战+赚钱】小程序源码
回复 【python】学微获取全套0基础Python知识手册
回复 【2019】获取2019 .NET 开发者峰会资料PPT
回复 【加群】加入dotnet微信交流群

输入任意文字即可激活,这款软件爱了!


人人影视字幕组凉了,这款美剧APP不能错过!



浏览 29
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报