一:背景

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

记一次 .NET 医院CIS系统 内存溢出分析的更多相关文章

  1. 记一次 .NET 某妇产医院 WPF内存溢出分析

    一:背景 1. 讲故事 上个月有位朋友通过博客园的短消息找到我,说他的程序存在内存溢出情况,寻求如何解决. 要解决还得通过 windbg 分析啦. 二:Windbg 分析 1. 为什么会内存溢出 大家 ...

  2. 记一次 .NET 某三甲医院HIS系统 内存暴涨分析

    一:背景 1. 讲故事 前几天有位朋友加wx说他的程序遭遇了内存暴涨,求助如何分析? 和这位朋友聊下来,这个dump也是取自一个HIS系统,如朋友所说我这真的是和医院杠上了,这样也好,给自己攒点资源, ...

  3. java内存溢出分析工具

    http://www.cnblogs.com/preftest/archive/2011/12/08/2281322.html java内存溢出分析工具:jmap使用实战 在一次解决系统tomcat老 ...

  4. Java 内存溢出分析

    原文地址:Java 内存溢出分析 博客地址:http://www.moonxy.com 一.前言 Java 的 JVM 的内存一般可分为 3 个区:堆(heap).栈(stack)和方法区(metho ...

  5. jvm内存溢出分析

    概述 jvm中除了程序计数器,其他的区域都有可能会发生内存溢出 内存溢出是什么? 当程序需要申请内存的时候,由于没有足够的内存,此时就会抛出OutOfMemoryError,这就是内存溢出 内存溢出和 ...

  6. java内存溢出分析(二)

    我们继续java内存溢出分析(一)的分析,点击Details>按钮,显示如下图,我们发现有一个对象数量达到280370216个,再点击其中的List objects 点击后,显示下图 至此,我们 ...

  7. jmap的使用以及内存溢出分析

    一.jmap的使用以及内存溢出分析 前面通过jstat可以对jvm堆的内存进行统计分析,而jmap可以获取到更加详细的内容,如:内存使用情况的汇总.对内存溢出的定位与分析 1.查看内存使用情况 jma ...

  8. eclipse memory analyzer对系统内存溢出堆文件解析0(转)

    前言 在平时工作过程中,有时会遇到OutOfMemoryError,我们知道遇到Error一般表明程序存在着严重问题,可能是灾难性的.所以找出是什么原因造成OutOfMemoryError非常重要.现 ...

  9. eclipse memory analyzer对系统内存溢出堆文件解析(转)

    本文转之:https://blog.csdn.net/rachel_luo/article/details/8992461 前言 性能分析工具之-- Eclipse Memory Analyzer t ...

随机推荐

  1. FreeBSD 发布 2020 年 Q3 季度报告

    FreeBSD 几日前发布 Q3 季度报告,介绍了在过去第三季度里 FreeBSD 完成的工作和相关项目,涉及到架构支持.内核改进.持续集成和驱动程序优化等. 列举部分如下: FreeBSD 基金会目 ...

  2. org.springframework.web.util.IntrospectorCleanupListener作用

    回收那些不会主动回收,导致内存泄漏的垃圾,如javabeans

  3. 官方正式发布 Java 16

    前言 就在2021/03/16,官方正式发布了Java 16.我们可以下载使用Java 16了. 特性 向量API(孵化) 在运行期,Vector 表示向量计算可以可靠地编译成支持CPU架构上的最佳矢 ...

  4. DevExpress主要常用控件

    DevExpress主要常用控件说明:1. TestEdit: 一个单行文本编辑器. 常用属性:Name:该控件的名称.Text:该控件中的内容.Enabled:该控件是否激活. Visible:控件 ...

  5. 一次VLAN标签引发的网络事件的处置

    一次VLAN标签引发的网络事件的处置 一.背景介绍 事件背景: HZ某分公司新装一套业务系统,通过一条专线和BJ总公司连通.分配给HZ公司的ip地址为:a.b.c.X,掩码24位,网关a.b.c.1. ...

  6. 2021年的UWP(6)——长生命周期Desktop Extension向UWP的反向通知

    上一篇我们讨论了UWP和Desktop Extension间的双向通讯,适用于Desktop Extension中存在用户交互的场景.本篇我们讨论最后一种情况,与前者不同的是,Desktop Exte ...

  7. 开源项目月刊《HelloGitHub》第 60 期

    兴趣是最好的老师,HelloGitHub 就是帮你找到兴趣! 简介 分享 GitHub 上有趣.入门级的开源项目. 这是一个面向编程新手.热爱编程.对开源社区感兴趣 人群的月刊,月刊的内容包括:各种编 ...

  8. golang 性能优化分析:benchmark 结合 pprof

    前面 2 篇 golang 性能优化分析系列文章: golang 性能优化分析工具 pprof (上) golang 性能优化分析工具 pprof (下) 一.基准测试 benchmark 简介 在 ...

  9. 文件连接--ln

    ln -n file1 file2  将文件2设置为文件1的软连接:file1和file2 任何一个改动都会反馈到另一方,删除源文件, 软连接文件不可用 ln -s file1 file2  将文件2 ...

  10. 分库分表之后,id主键如何处理?

    (1)数据库自增id 这个就是说你的系统里每次得到一个id,都是往一个库的一个表里插入一条没什么业务含义的数据,然后获取一个数据库自增的一个id.拿到这个id之后再往对应的分库分表里去写入. 这个方案 ...