分析性能瓶颈: 调试OutOfMemoryException

在前面的文章里面,执行性能测试—起步里,讲了执行性能测试的基本步骤,而且在前面的 例子里面,通过一个2M多的文本文件,对比了冒泡排序和快速排序的性能之间的差别。但是当 我使用一个700M大小的文本文件进行测试的时候—因为我需要了解在极端情况下两种排序方法 的差别。原定是2G的文本文件,但是无论快排还是冒泡排序都要求被排序的数据完全存在于内 存当中,对于32位机,2G的数据是一个上限,因为操作系统的内核代码使用掉了另外2G的地址 空间—除非你使用/LARGEADDRESSAWARE这个开关,限制操作系统只使用1G的内存,而让用户态 代码使用3G的空间。

为了重现这个问题,我们先来准备一下数据,用下面两个DOS命令就可以准备好这些数据了 :

1.dir /s /b c:\windows > d:\test.txt

2.FOR /L %i IN (1,1,100) DO type test.txt >> testdata.txt

第一个命令将Windows文件夹里面所有子文件夹的文件列表都输出到test.txt文件里,第二 个命令循环100遍,将test.txt文件内容追加到testdata.txt里面,这样就可以生成好几百兆大 小的文本文件了。

数据生成好了以后,使用执行性能测试—起步贴出来的程序执行一遍,就可以看到 OutOfMemeoryException了(如果你没有看到这个异常,可能是你的机器太强大了,请换一个更 大的文件)。在调试器里面执行后出现异常后,加载进SOS进行分析:

 (318.1688): CLR exception - code e0434352 (first chance)
… …
#
# OutOfMemeoryException已经抛出了
#
  (318.1688): CLR exception - code e0434352 (!!! second chance !!!)
eax=0017eb74 ebx=00000005 ecx=00000005 edx=00000000 esi=0017ec20  edi=005595e0
eip=753b9617 esp=0017eb74 ebp=0017ebc4 iopl=0         nv up ei pl  nz ac pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000              efl=00000216
KERNELBASE!RaiseException+0x54:
753b9617 c9              leave
0:000> .loadby sos clr
0:000> !pe
Exception object: 744a8e84
Exception type:   System.OutOfMemoryException
Message:          <none>
InnerException:   <none>
StackTrace (generated):
SP       IP       Function
#
# 不出所料,StreamReader里面使用StringBuilder将文本文件读入到一个字符串里。而 StringBuilder是动态
# 分配内存的。
#
     0017ED38 5713E61E mscorlib_ni!System.Text.StringBuilder.ToString() +0x1e
     0017ED64 57121991 mscorlib_ni!System.IO.StreamReader.ReadToEnd() +0x7d
     0017ED78 002F0136 ConsoleApplication1! ConsoleApplication1.Program.Main(System.String[])+0xc6

StackTraceString: <none>
HResult: 8007000e
#
# 使用AnalyzeOOM来分析一下原因,看看是GC哪一个内存区域导致了这个异常。
#
0:000> !ao
#
# 找到了,是大对象堆(Large Object Heap - LOH),GC在尝试申请一个1.5G的内存 空间时
# 遭拒。
#
Managed OOM occured after GC #312 (Requested to allocate 1649667816  bytes)
Reason: Didn't have enough memory to allocate an LOH segment
Detail: LOH: Failed to reserve memory (1652555776 bytes)
#
# 使用!eeheap –gc命令找到GC里,各个generation的堆的起始地址和结束地址
#
0:000> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x744a8e78
generation 1 starts at 0x7446a408
generation 2 starts at 0x01911000
ephemeral segment allocation context: none
  segment     begin allocated size
01910000 01911000 0290aee0 0xff9ee0(16752352)
03fd0000 03fd1000 04fcd3ec 0xffc3ec(16761836)
… …
73ca0000 73ca1000 744aae84 0x809e84(8429188)
Large object heap starts at 0x02911000
#
# LOH的起始地址找到了,0x02911000就是LOH的起始地址
# 已经分配了0x02913240个字节,所以结束地址就是
# 0x02911000 + 0x02913240
#
  segment     begin allocated size
02910000 02911000 02913240 0x2240(8768)
Total Size:              Size: 0x629424c0 (1653875904)  bytes.
------------------------------
GC Heap Size:            Size: 0x629424c0 (1653875904)  bytes.
#
# 使用dumpheap看一下LOH当中各个对象的内存分配情况。
#
# dumpheap的参数中,0x02911000是要查看的内存的起始地址,
# 而0x02911000 + 0x02913240是查看的结束地址
#
0:000> !dumpheap -stat 02911000 02911000+02913240
total 0 objects
Statistics:
       MT    Count    TotalSize Class Name
00529748        4           84      Free
57166c28        3         8720 System.Object[]
#
# 不出所料,StringBuilder和Char[]对象最多,但是
# Char[]的数组大小有171M之多。
#
571afb78     1119        31332 System.Text.StringBuilder
571b1d88     1120     17933440 System.Char[]
Total 2246 objects
#
# 使用!dumpheap的-mt参数看看char[]数组的详细内存分配情况
# 
# 571b1d88这个参数,来自于前面的!dumpheap命令的输出(注意高亮显示的地方)
# 这个参数告诉dumpheap命令,只检索char[]数组(Method table是571b1d88)
# 的情况。
#
0:000> !dumpheap -mt 571b1d88 02911000 02911000+02913240
  Address       MT     Size
03fd1000 571b1d88    16012
… …
05222c90 571b1d88    16012
total 0 objects
Statistics:
       MT    Count    TotalSize Class Name
571b1d88     1120     17933440 System.Char[]
Total 1120 objects
#
# 随便选一个对象(注意上面一个高亮显示的文本),看看它到底被谁引用了,
# 导致GC一直不释放它,毕竟我电脑的有2G的物理内存,读取几百兆的文件,
# 就触发了OutOfMemoryException,的确有点离谱。
#
0:000> !gcroot 03fd1000
Note: Roots found on stacks may be false positives.Run "!help gcroot"  for
more info.
Scan Thread 0 OSTHread 1688
ESP:17eca4:Root: 0191d2e0(System.Text.StringBuilder)->
  744a4fd0(System.Text.StringBuilder)->
  … …
  5de44814(System.Text.StringBuilder)->
Command cancelled at the user's request.
  03fd1000(System.Char[])
#
# 再看看0x0191d2e0这个StringBuilder对象都在哪些地方被引用到了,
# 根据GC的实现,堆栈上各个函数的局部变量是当作root来处理的
#
0:000> !gcroot 0191d2e0
Note: Roots found on stacks may be false positives.Run "!help gcroot"  for
more info.
Scan Thread 0 OSTHread 1688
ESP:17eca4:Root: 0191d2e0(System.Text.StringBuilder)
ESP:17eca8:Root: 0191d2e0(System.Text.StringBuilder)
ESP:17ecb4:Root: 0191d2e0(System.Text.StringBuilder)
ESP:17ecbc:Root: 0191d2e0(System.Text.StringBuilder)
ESP:17ed3c:Root: 0191d2e0(System.Text.StringBuilder)
ESP:17ed58:Root: 0191d2e0(System.Text.StringBuilder)
Scan Thread 2 OSTHread ce8
#
# 从前面的输出,随便找两个对象(前面标注的文本),看看它们都是哪些函数的局部变量
# 例如17ed58这个地址介于System.IO.StreamReader.ReadToEnd()和
# System.Text.StringBuilder.ToString()之间,也就是StreamReader.ReadToEnd()
# 这个函数定义的,至此,基本上我们可以认为已经找到影响性能的元凶了。
#
0:000> !dumpstack
OS Thread Id: 0x1688 (0)
Current frame: KERNELBASE!RaiseException+0x54
ChildEBP RetAddr Caller, Callee
0017eb7c 753b9617 KERNELBASE!RaiseException+0x54, calling ntdll! RtlRaiseException
0017eba0 5888bc5e clr!DllUnregisterServerInternal+0x15c62, calling clr! DllUnregisterServerInternal+0xa55b
0017ebac 588fa99c clr!LogHelp_TerminateOnAssert+0x2df44, calling clr! DllUnregisterServerInternal+0x15c45
0017ebbc 58871bd0 clr+0x1bd0, calling clr+0x1b8b
0017ebc4 588fac08 clr!LogHelp_TerminateOnAssert+0x2e1b0, calling KERNEL32! RaiseException
0017ec54 5896ab0b clr!CopyPDBs+0x4abd, calling clr! LogHelp_TerminateOnAssert+0x2e03e
0017ec90 58b27c9e clr!CorLaunchApplication+0x11756, calling clr! CopyPDBs+0x4a78
0017ecdc 588908f6 clr!CoUninitializeEE+0x272e, calling clr! GetMetaDataInternalInterface+0xde18
0017ed30 5713e61e (MethodDesc 56f0c09c +0x1e  System.Text.StringBuilder.ToString()), calling 00242350
0017ed5c 57121991 (MethodDesc 56efff40 +0x7d  System.IO.StreamReader.ReadToEnd())
0017ed70 002f0136 (MethodDesc 00253840 +0xc6  ConsoleApplication1.Program.Main(System.String[]))
0017edd4 588721db clr+0x21db
0017ede4 58894a2a clr!CoUninitializeEE+0x6862, calling clr+0x21a8
… …
0017f91c 589daf00 clr!CorExeMain+0x1c, calling clr!GetCLRFunction+0xd6a
0017f954 718455ab mscoreei!CorExeMain+0x38
0017f960 6f667f16 MSCOREE!CreateConfigStream+0x13f
0017f970 6f664de3 MSCOREE!CorExeMain+0x8, calling MSCOREE! CorExeMain+0x2f14
0017f978 75d41194 KERNEL32!BaseThreadInitThunk+0x12
0017f984 7723b495 ntdll!RtlInitializeExceptionChain+0x63
0017f9c4 7723b468 ntdll!RtlInitializeExceptionChain+0x36, calling ntdll! RtlInitializeExceptionChain+0x3c

时间: 2024-09-12 01:20:58

分析性能瓶颈: 调试OutOfMemoryException的相关文章

分析、调试内存泄漏的应用程序

<!-- /* Font Definitions */ @font-face {font-family:Wingdings; panose-1:5 0 0 0 0 0 0 0 0 0; mso-font-charset:2; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:0 268435456 0 0 -2147483648 0;} @font-face {font-family:宋体; pan

听阿里巴巴JVM工程师为你分析常见Java故障案例

本文根据12月23日阿里巴巴技术保障部JVM组软件工程师陆传胜老师的主题分享整理!小编特别整理出其中精华内容,供大家学习交流. 目录 HotSpot常识 Java故障排查方法论 Java故障案例分析 Part 1    HotSpot常识    HotSpot是目前最常见的开源JVM(GPL协议),用来运行Java应用和applet,本次讨论基本都是基于这一软件来进行的.   所有的Java对象都是分配在Java堆上的,Java代码中看到的引用,在JVM的实现中就是一个指针,指向一段被表示成对象

如何使用工具进行线上 PHP 性能追踪及分析?

工作了一两年的 PHPer 大概都多多少少知道一些性能分析的工具,比如 Xdebug.xhprof.New Relic .OneAPM.使用基于 Xdebug 进行 PHP 的性能分析,对于本地开发环境来说是够用了,但如果是线上环境的话,xdebug 消耗较大,配置也不够灵活.相比 Xdebug ,xhprof 性能消耗较小,但是 xhprof 注入代码后我们还需要实现保存 xhprof 数据以及展示数据的 UI,听起来似乎又是一大堆工作. 很多人都知道,New Relic 和 OneAPM 是

大型挂马团伙“擒狼”攻击分析及溯源报告

本文讲的是大型挂马团伙"擒狼"攻击分析及溯源报告, 第一章 概述 7月13日,360安全卫士检测到一起网站广告位挂马事件,大量网络广告出现集体挂马,广告内容以同城交友等诱惑信息为主,预警为"擒狼"木马攻击.我们通过对整个挂马攻击的分析溯源发现,这个木马主要功能是锁定浏览器的主页并带有远程控制后门,作者通过木马谋取暴利,是一起典型的黑产行为.  该木马通过漏洞执行,安装服务和驱动,通过驱动锁定浏览器主页,服务实现自启动并将自身注入系统进程.连接C&C下载配置和

针对CVE-2015-2545漏洞研究分析

本文讲的是针对CVE-2015-2545漏洞研究分析, 1. 概述 这是一种MSOffice漏洞,允许通过使用特殊的 Encapsulated PostScript (EPS)图形文件任意执行代码.这种漏洞于2015年3月被发现,漏洞未修补情况持续了4个月.之后,微软发布了修复补丁(MS15-099),解决了这一安全问题. 漏洞发布时间:2015-09-08     漏洞更新时间:2015-09-08 影响系统 Microsoft Office 2007 SP3 Microsoft Office

能测试知多少--系统计数器与硬件分析

性能计数器(Performance Counter),也叫性能监视器.一个人健康状况如何,我们通过对其做各项体检获得相关的状况指标,如血压.心跳,肺活量等.那么在做性能测试过程中,整个系统的软硬件进行监控也必不可少,监控所获得的数据也是我们分析系统性能的主要依据. 在整个系统中,对于不同的软件和硬件,我们对其监控的指标也不一样,就像一个公司中的所有人员,其每个人的职责不同,评判和考核的标准也是不一样的.下面将从系统的各个方面进行分析. 操作系统性能计数器 操作系统监控器,主要监控操作系统级别上的

Shell脚本查看linux系统性能瓶颈

  linux服务器敲命令反应慢,网站访问慢,到底什么情况?根据本人的经验,主要原因可能是系统资源到达瓶颈,已经无法处理更多请求.在有监控系统情况下,可以直接通过WEB页面可视化看出是CPU瓶颈?硬盘瓶颈?还是网络瓶颈?如果公司服务器较少或者云服务器,就有可能没有一套监控系统,这时就要登陆到服务器,一条一条的敲命令,查找分析性能瓶颈.命令这么多,咋记得住啊!就算记得住,输入也费劲,于是就有了这个脚本,为了以后自己使用,另外也想分享给博友,学shell朋友能从中得到一丢丢启发.写的比较仓促,内容有

第二章排错的工具:调试器Windbg(上)

感谢博主 http://book.51cto.com/art/200711/59731.htm <Windows用户态程序高效排错>第二章主要介绍用户态调试相关的知识和工具.本文主要讲了排错的工具调试器Windbg.     第二章 汇编.异常.内存.同步和调试器--重要的知识点和神兵利器 这一部分主要介绍用户态调试相关的知识和工具.包括汇编.异常exception.内存布局.堆heap.栈stack.CRTC Runtime.handle/Criticalsection/thread con

《Effective Debugging:软件和系统调试的66个有效方法》——第16条:使用专门的监测及测试设备

第16条:使用专门的监测及测试设备 调试嵌入式系统及系统软件的时候,我们可能要对从硬件到应用程序的整个计算栈进行分析.调试工作一旦深入硬件层面,我们就需要关注电流的微小变化以及磁矩的对齐情况等细节.在大多数情况下,可以通过强大的IDE以及一些追踪软件与日志记录软件来探查这些问题,然而有的时候,就连这些工具也帮不上忙.这通常发生在软件与硬件有所接触的场合,也就是说,虽然你认为你所写的软件能够像预期的那样运作,但是硬件却有着它自己的处理方式.例如,你把正确的数据写入磁盘,再将其读取出来,却发现这些数