AQTime进行内存泄露和资源泄漏监控.docx
《AQTime进行内存泄露和资源泄漏监控.docx》由会员分享,可在线阅读,更多相关《AQTime进行内存泄露和资源泄漏监控.docx(16页珍藏版)》请在冰豆网上搜索。
AQTime进行内存泄露和资源泄漏监控
利用AQTime分析.NET程序内存泄露
1.AQTime简介
AQTime是一款著名的,功能强大的CodeProfiler工具,它与TestComplete(自动化测试工具)一样,是同属于AutomatedQA公司旗下的软件测试产品,产品含有完整的性能和内存,资源调试工具集,并支持32位及64位机器上的Windows,.NET和JAVA应用程序,同时还支持VBScript和Jscript代码调试。
AQTime可以帮助程序员理解程序执行过程中运行情况,它内置了大量的调试方案,以及显示面板帮助调试人员隔离以及消除程序的性能问题以及内存、资源泄漏问题,AQTime即可以作为单独的程序启动也可以集成到MicrosoftVisualStudio或者EmbarcaderoRADStudio(Delphi和C++Builder的集成开发环境)运行.
AQTime的主要功能
●性能测试
●内存使用情况监控
●系统资源使用情况监控
●代码覆盖率监控
●兼容性分析
●程序异常,模块加载,函数调用情况监控
AQTime内置有丰富的调试方案,分为5类(Allocation、Coverage、Performance、StaticAnalysis、Tracing),共14种调试方案工具集,列举如下:
●PerformanceProfiler性能调试方案
提供针对程序中任意范围内的程序段或单行代码进行的性能检测。
在程序执行期间,工具自动收集运行时的性能表征数据(如调用次数、执行时间、调用与被调用函数、函数调用层次图、执行期间发生的异常情况等等)。
此外,工具提供与运行时间、CPU缓存使用等相关的多种计数器[如ElapsedTime、UserTime、User+KernelTime、CPUCacheMisses、ContextSwitches等],为了解应用代码级和应用程序级上的性能使用和确定可能存在的性能瓶颈提供详实的参考数据。
●AllocationProfiler内存使用分析方案
跟踪程序执行过程中对内存资源的使用情况,按类、对象检测并显示程序中对内存资源的使用情况,确定明确或潜在的内存泄漏来源,避免由此造成的程序问题。
●BDESQLProfilerDelphi的BDE桌面数据库调试方案
●CoverageProfiler代码覆盖率调试方案
代码覆盖率分析,测试出代码在运行过程中的执行情况,可以帮助查找出程序中的无用代码。
●ExceptionTraceProfiler异常跟踪方案
监测程序运行期间出现的预知和未知异常,并将异常在源代码中进行定位。
当程序出现异常时,EventLog窗口会记录下来,再EventView窗口选中异常查看,在Editor窗口中可以查看到当前异常所在的代码位置。
●LightCoverageProfiler轻度代码覆盖率调试方案
●FunctionTraceProfiler函数跟踪方案
●LoadLibraryTrace加载库跟踪方案
●PlatformComplianceProfiler平台兼容性测试方案
●ReferenceCountProfiler引用数测试方案
●ResourceProfiler资源调试方案
跟踪程序运行期间,对操作系统资源(如fonts,brushes,bitmaps,andothergraphiccomponents,registry,COMobjects,printspooler,)的使用情况(如某时间片内,程序本身开销的系统资源、因这些资源带来的内存和CPU开销、来自于这些资源使用中出现的错误)使检测者很容易获知在程序运行期间与之相关系统资源的分配和使用情况。
●SequenceDiagramLinkProfiler序列图测试方案
自动识别出待测程序中的函数调用关系,自动生成UML格式时序图,并通过word或visio格式显示,便于检测者及时检查程序的行为。
●StaticAnalisysProfiler静态分析方案
静态分析并标识出待测程序的内部结构(如UML时序图、函数间调用关系、存在的循环语句、判断语句、异常处理)和程序规模。
●UnusedVCLUnits未使用VCL单元测试方案
以上的这么些个调试方案,对于.NET内存调试主题来说,AllocationProfiler内存使用分析方案最为重要和关键,对应辅助的还有调试系统资源引用泄漏的调试方案-ResourceProfiler资源调试方案。
其他几种测试方案各有其特殊的用途,据悉,技术中心最近正在通过AQTime做ZLHIS的代码覆盖率测试。
“工欲善其事必先利其器“,随着对类似AQTime这种业内各著名测试工具的深入理解和应用,相信会对测试和代码调试工作都会有很大的帮助,大大提高我们软件产品的质量以及提高开发及测试工作的效率。
2..NET程序内存调试
2.1.建立调试工程
首先建立AQTime工程,因为是做ZLBH的内存分析,这里我需要在AQTime的方案列表中选择”Allocation”调试工具集下的”AllocationProfiler”方案.
选择ProfileEntire.NETCodebyClasses,下面有三个测试级别:
●byRoutines按方法,以方法或函数作为单位
●byLine按代码行作为单位
●byClasses按类作为单位
在”AllocationProfiler”方案只支持ByClasses,ByRoutine,ByLine在Performance和Coverage中支持.
AQTime和其他的MemoryProfiler不同的地方是,AQTime可以根据Modules为单位进行调试的,即内存报告监控的范围只限于你所选定的模块,这样方便我们收集数据以及分析定位问题,而模块的范围包括exe,dll,ocx,bpl(delphi的控件文件)四种类型.而我们所熟悉的其他MemoryProfiler工具,比如:
.NetMemoryProfiler,ANTsMemoryProfiler,通常只需要指定启动的主程序EXE文件即可。
通过选择工具栏上的选项,设置“显示所有加载的对象“工具栏选项,取消”只显示项目中指定的对象类型“工具栏选项,将取消默认的只显示加载的模块的设置。
根据实际的操作,推荐不用选中太多的模块,对于像ZLBH这种大型应用,全生命周期收集分析下来,启动就需要几分钟,收集数据会很慢,很慢,而且还会收集一些不需要的垃圾数据,而且在分析的过程中过滤起来也超慢,所以推荐在分析前先控制好模块的范围。
AQTime的这种分模块调试的方式应该是为了适应于其他几种调试方式,比如:
代码覆盖调试等,对于内存调试方案的好处在于,有助于按需要跟踪指定的模块的内存占用情况,而不是限制于整个应用程序域下所有的对象.
这里,我们需要将ZLBH程序目录下所有的ZLSoft.BusinessHome命名空间下的Dll文件全部加入到新建的工程下面.
建立好工程后,就可以直接点击”Run”或者按F5热键运行需要调试的程序了.
2.2.监控运行状态
在实际的运行状态下,AQTime可以通过多个视图实时监控程序的运行状态,其中AQTime提供的监控视图有:
●EventView运行事件视图
EventView视图中记录在被调试程序在运行过程中如下信息:
●模块加载,模块卸载,模块移除,模块激活,模块添加情况
●线程创建,退出情况
●程序异常情况
●进程创建,进程退出情况
●UserBreakPoint,用户设置断言的执行情况
●Monitor监视器视图
监视器视图显示当前程序的所使用到的类的数量及占用内存情况
ClassName
对象类型名称
LiveCount
在程序中存活的对象数目
LiveSize
在程序中存活的对象占用内存的大小
ModuleName
对象类型所在的模块名称
在右侧还有程序整体占用内存字节数的动态图,可以动态监控程序一段时间的内存占用变化。
●Disassembler反汇编视图
用于检查程序Routines的二进制代码并依赖于编译器,只有当选择了”ByRoutines”选项才会出现Dissembler视图内容。
●Editor代码编辑器视图
设置了“ProjectSearchDirectories”和“SearchDirectories”之后,AQTime会自动在指定的目录下搜寻代码,选择Routine后,会将当前执行的Routine所在的代码行代码内容显示出来。
●Details明细视图
明细视图用于显示在报告面板中选中的行所附加的调试信息,每种调试方案对应于不同的明细视图列表,例如:
在AllocationProfiler,Detail视图显示以下几列:
RoutineName
方法名称
SourceFile
源文件名称
HitCount
击中数目
SourceLine
源代码行数
ClassName
类型名称
RoutineName从下到到上是当前的方法所在的调用堆栈信息,可以通过调用堆栈信息清晰的看出当前程序的执行路径,以及执行路径所执行的次数和当前方法路径所在的代码行数。
●CallGraph对象关系引用图视图
CallGraph显示对象引用层级,可以通过点击不同的对象遍历出对象引用的路径。
下图为SmartForm的一个实例的对象关系引用图,其中SmartForm.3935624实例被标记为Root=True,表明这个实例对象被根化,对象被根化的原因有以下三个:
1.被全局对象引用
2.被其他的本地变量所引用
3.对象作为一个方法的参数被引用
Root=False的对象表明对象被另一个对象作为属性引用。
●CallTree对象引用树视图
该视图只限于托管代码,对象引用树视图显示两个页卡:
“ReferencesTo“和”ReferenceFrom“,”ReferencesTo”表示的是引用到当前对象的其他对象的信息,“ReferencesFrom”以指定对象为起点,当前对象所引用到的对象。
2.3.实际调试
以上对AQTime调试步骤以及监控方法做了些探索,实际运用还是需要通过有目的的实际调试分析过程来总结经验和方法。
下面通过几个对常见造成内存泄漏问题的现象进行分析如下:
2.3.1.自定义事件-委托造成的内存泄漏
委托和自定义事件在.NET编程中重要的特性之一,委托(Deletgate)是.NET事件机制的关键,但是如果使用不当则会造成隐性的内存泄漏问题,发生这种情况的主要场景为需要添加Eventhandler到一个Event上,使用完后又忘了去移除它,导致事件中的对象无法被释放掉。
上图描述的是一个对象定义了一个事件,新建一个Form窗体并挂接到这个事件上,当关闭这个Form窗体后,应该会被释放并被GC回收,而实际上,这个事件的代理仍然保留着对这个Form对象的强引用从而导致不能被正常回收,导致内存泄漏。
对应的实验的代码如下:
新建Form2窗口,并添加如下代码
publicForm2()
{
byte[]m_ExtraMemory=newbyte[1000000];
InitializeComponent();
}
publicvoidEventTriggred(objectsender,EventArgse)
{
}
定义一个TestEventClass类
publicclassTestEventClass
{
publiceventEventHandlerDoEventHandler;
publicvoidOnDoEventHandler(EventArgsargs)
{
if(DoEventHandler!
=null)
{
OnDoEventHandler(null,args);
}
}
publicvoidOnDoEventHandler(objectsender,EventArgse)
{
MessageBox.Show("事件执行");
}
}
然后在Form1上通过按钮点击事件Show或ShowDialog函数显示Form2。
TestEventClassa=newTestEventClass();
privatevoidbutton1_Click(objectsender,EventArgse)
{
Form2frm=newForm2();
a.DoEventHandler+=newEventHandler(frm.EventTriggred);
frm.Show();
}
OK!
准备就绪,开始实验,用AQTime设置好“AllocationProfiler”,并选中“ProfileEntire.NETCodeByClass”,执行实验程序,首先通过Form1上的一个Button,执行Form2的弹出然后关闭Form2。
完成后,执行AQTime的“GetResult”得到收集到的实例如下:
可以看到Form2在四次打开和关闭后,LiveCount仍然为4,说明Form2在关闭后并没有被回收。
在看AQTime的CallTree视图,可以看到Form2仍然被TestEventClass.2868实例所引用到,从而导致Form2本身及其所引用的其他对象无法被正常回收。
出现这种情况的原因是,将一个生命周期较短的对象(Form2)注册到一个生命周期较长(Form1)的某个事件(TestEventClass)上,两者便无形之间建立一个引用关系(Form1引用Form2)。
这种引用关系导致GC在进行垃圾回收的时候不会将Form2是为垃圾对象,最终使其常驻内存(或者说将Form2捆绑到Form1上,具有了和Form1一样的生命周期)。
同理,静态事件也会出现类似的内存泄漏问题。
用一个图表示,如下:
.NET的Delegate对象同样可以分解成两个部分:
委托的功能(Method)和目标对象(Target),Target即委托的对象,在例子中DoEventHandler的Target就是Form2,从上图中可以看到TestEventClass的DoEventHanlder的Target对Form2造成了强引用。
解决的方法有两个,第一个就是通过对DoEventHandler加入-=卸载所加载的事件,加入下面一行代码
a.DoEventHandler-=newEventHandler(frm.EventTriggred);
第二个方法就是采用WeakReference(弱引用)机制,让EventHandler通过WeakReference的方式与事件监听者(Form2)建立弱引用关系,当不用到事件监听者的时候使其能够被垃圾回收,至于代码如何编写,请兴趣的读者可以参考网文《SolvingtheProblemwithEvents:
WeakEventHandlers》,这里就不再赘述。
2.3.2.非托管资源造成的内存泄漏
在.NET开发中容易被忽视,引起内存泄漏通常存在于以下三种情况:
1.对象被引用而没有被释放
2.没有释放非托管资源
3.没有释放非托管资源封装对象
上个章节描述的EventHanlder和Delegate造成的内存泄漏就属于第一类,之所以要单独拿出来叙述是因为.NET事件和代理造成的内存泄漏相对于静态变量的根化引用更容易被忽略且不好被识别出来。
第2类和第3类同属于系统资源类型造成的内存泄漏,但是又有所区别。
第二类是指通过本地API函数与托管对象进行交互(比如:
通过P/Invoke方式调用本地DLL,DLLImport声明静态外部函数和COMInterop)所用到的非托管资源。
例如:
当通过DLLImport调用API函数GetDC函数时忘了调用ReleaseDC去释放设备句柄造成4个字节的内存泄漏。
再如:
智能文档中使用的Word以及导出EXCEl功能用到的Office的COM非托管组件,在关闭时GC不能识别COM组件而造成有时候无法对COM对象进行释放,这时候可以通过以下两个InteropServices函数进行释放
●System.Runtime.InteropServices.Marshal.ReleaseComObject(comObject);
●System.Runtime.InteropServices.Marshal.FinalReleaseComObject(comObject);
上次在敏捷交流了内存相关事项问题后,给大家留了几道思考题,其中第一道题是“数据库连接SqlConnection是不是非托管资源,为什么?
”,有些人的回答是“肯定”,之所以有这样回答是因为大家所了解的非托管资源的经典认知就是数据库连接、文件、网络连接都是非托管资源,有人认为SqlConnection就是数据库连接,其实不然,.NET对某些非托管资源提供一种包装类,SqlConnection就是这种,包装类的源(WrapSource)才真正是托管资源,它管理了非托管资源,而它本身确实托管的。
.NETGDIPlus中常用的Drawing命名空间下的类很多就是这种包装类型,现将常用的几种非托管包装类列举如下:
ApplicationContext
Component
ComponentDesigner
Brush
Container
Context
Cursor
FileStream
DataSet
Font
Icon
Image
Matrix
Texture
OdbcDataReader
OleDBDataReader
Pen
Regex
Socket
StreamWriter
Timer
Tooltip
Bitmap
识别这种包装类型的主要方法就是通过MSDN查询是否该对象继承于System.MarshalByRefObject类。
做一个实验来测试Graphics的释放,新建一个Form对象,在Form对象的Paint事件里,写入以下代码,用于在Form2上绘制。
Bitmapbmp=newBitmap(600,600);
Graphicsg=Graphics.FromImage(bmp);
Brushbrush=newLinearGradientBrush
(newPointF(0.0f,0.0f),
newPointF(700.0f,300.0f),
Color.Blue,Color.Red);
for(intj=0;j<60;++j)
for(inti=0;i<60;++i)
g.FillEllipse(brush,i*10,j*10,10,10);
this.CreateGraphics().DrawImage(bmp,0,0);
运行起来发现,不停的移动Form2,对应刷新Form2的Paint事件,在内存管理器里面可以看到实验程序的内存会不停的增长,这说明这时候已经产生了内存泄漏了。
启动AQTime,并启用ResourceProfiler调试方案,运行程序,隔一段时间调用“GetResult”收集数据。
第一次收集数据,GpGrahics对象的LiveCount=3;
第二次收集数据,GpGrahics对象的LiveCount=21;
GpGraphics对象的持续增长说明,GpGraphics造成了内存泄漏,再利用.NETMemoryProfiler捕捉内存Heap快照。
显示方法中的Bitmap、Graphics、LinearGradientBrush三种类型出现了“UndisposedInstances”警告。
这里,因为Graphics没有释放导致Grahics上引用的Bitmap,以及Bimap上的LinearGradientBrush对象都没有被及时释放,造成内存泄漏。
将代码修改如下:
using(Graphicsg1=this.CreateGraphics())
{
using(Bitmapbmp=newBitmap(600,600))
{
using(Graphicsg=Graphics.FromImage(bmp))
{
using(Brushbrush=newLinearGradientBrush
(newPointF(0.0f,0.0f),
newPointF(700.0f,300.0f),
Color.Blue,Color.Red))
{
for(intj=0;j<60;++j)
for(inti=0;i<60;++i)
g.FillEllipse(brush,i*10,j*10,10,10);
}
g1.DrawImage(bmp,0,0);
}
}
}
再运行AQTime和.NETMemoryProfiler,可以看到.NETMemoryProfiler的警告消除了,AQTime显示GpsGraphics的LiveCount一直是1,不再会增加。
由此得知,.NET中的Drawing托管对象使用也会造成内存泄漏,以上泄漏的问题很容易被忽视,因为如果这种泄漏内存的增长量不大,在整个程序运行时显得微不足道,而不容易察觉,另外因为Form作为继承了Idisposable接口的控件容器,在其关闭时会自动调用其每个被引用对象的Dispose方法,所以最后Form也会帮你回收的。
Form的Dispose方法如下:
protectedoverridevoidDispose(booldisposing)
{
if(disposing&&(components!
=null))
{
//释放每个被引用对象的Dispose方法
components.Dispose();
}
base.Dispose(disposing);
}
通常,好的编程习惯要求程序员在使用完非托管资源的对象后应尽快释放不再使用的对象和资源来避免潜在的内存泄漏。
释放这种包装对象的方法有3种:
●显式通过Dispose方法释放(推荐)
例如:
font.Dispose();
●隐式通过Using语句释放(推荐)
Using(Fontfont=newFont(Label1.Font.Name,currentSize,Label1.Font.Style))
{
}
●通过Finalization方法(不推荐)
不推荐,因为用 Finalize 方法回收对象使用的内存需要至少两次垃圾回收,当垃圾回收器回收时,它只回收没有终结器(Finalize方法)的不可访问的内存,这时他不能回收具有终结器(Finalize方法)的不可以访问的内存。
注:
什么是非托管资源
非托管资源是指CLR不能控制或管理的部分,这些资源一般不存在与Heap中,与非托管资源不同,托管资源一般指被CLR控制的内存资源,这些资源可以由CLR来控制