关于.net:记一次-NET-医院CIS系统-内存溢出分析

44次阅读

共计 5452 个字符,预计需要花费 14 分钟才能阅读完成。

一:背景

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

正文完
 0