内存泄漏检查教材.docx
《内存泄漏检查教材.docx》由会员分享,可在线阅读,更多相关《内存泄漏检查教材.docx(21页珍藏版)》请在冰豆网上搜索。
内存泄漏检查教材
内存泄漏检测方法
•对于不同的程序可以使用不同的方法来进行内存泄漏的检查,还可以使用一些专门的工具来进行内存问题的检查,例如MemProof、AQTime、Purify、BundsChecker等。
•也可以使用简单的办法:
利用Windows自带的Perfmon来监控程序进程的handlecount、VirtualBytes和WorkingSet3个计数器。
HandleCount记录了进程当前打开的句柄个数,监视这个计数器有助于发现程序是否存在句柄类型的内存泄漏;
VirtualBytes记录了程序进程在虚拟地址空间上使用的虚拟内存的大小,VirtualBytes一般总大于程序的WorkingSet,监视VirtualBytes可以帮助发现一些系统底层的问题;
WorkingSet记录了操作系统为程序进程分配的内存总量,如果这个值不断地持续增加,而VirtualBytes却跳跃式地增加,则很可能存在内存泄漏问题。
堆栈内存泄漏
•堆栈空间不足会导致在受托管的情况下引发StackOverflowException类型的异常,线程泄漏是堆栈内存泄漏的其中一种。
线程发生泄漏,从而使线程的整个堆栈发生泄漏。
•如果应用程序为了执行后台工作而创建了大量的工作线程,但却没有正常终止这些线程,则可能会引起线程泄漏。
一个堆栈内存泄漏的例子:
privatevoidbutton1_Click(objectsender,EventArgse)
{
//循环启动多个线程
for(inti=0;i<1500;i++)
{
Threadt=newThread(newThreadStart(ThreadProc));
t.Start();
}
}
staticvoidThreadProc()
{
Console.WriteLine("启动Thread#{0}",Thread.CurrentThread.ManagedThreadId);
//阻塞直到当前线程结束
Thread.CurrentThread.Join();
}
}
利用Perfmon检测线程堆栈泄漏
•默认堆栈大小为1MB,因此如果应用程序的PrivateBytes不断增大,同时.NETCLRLocksAndThreads中的#ofcurrentlogicalThreads也相应地增大,那么就很可能是发生了线程堆栈泄漏。
•可以利用Perfmon来判断是否存在内存泄漏现象。
执行被测试程序的相关操作,并在性能监视器中密切注意“PrivateBytes”和“#ofcurrentlogicalThreads”两个计数器的变化曲线,如果PrivateBytes不断增大,同时#ofcurrentlogicalThreads也相应地增大,则可判断程序发生了线程堆栈泄漏。
用CLRProfiler定位线程泄漏代码
利用CLRProfiler可以帮助检查程序是否存在线程泄漏。
方法如下:
(1)启动CLRProfiler
(2)单击“StartApplication”按钮
(3)选择需要测试的应用程序,单击“打开”按钮。
CLRProfiler会自动打开被测试程序,执行程序的相关操作,然后单击CLRProfiler的“ShowHeapNow”按钮
说明:
这个界面显示了程序的所有堆分配的情况。
其中可以看到线程类中分配了82K,占了18%以上,其中包含1500个线程对象。
(4)选中“Threading.Thread”的节点,单击右键,选择“ShowWhoAllocated”
说明:
在这个界面中可以看到是哪个类的哪个方法创建了这么多的线程对象,在这里可以看到是由button1_Click方法调用了线程类,从而定位到引发线程泄漏的代码。
资源泄漏
•资源通常指系统的对象。
例如GDI对象句柄、内存句柄等,在软件编程过程中,使用到很多这些资源对象,但是没有及时地释放掉就造成了资源泄漏。
•GDI泄漏是指程序申请了GDI句柄,但是没有及时释放,导致GDI句柄不断累积。
GDI泄漏可能导致系统不稳定,或者出现花屏。
一个GDI泄漏的例子:
•Form1:
•//调用Form2窗体
•Form2f=newForm2();
•//显示Form2窗体
•f.ShowDialog();
•Form2:
•privatevoidForm2_Load(objectsender,EventArgse)
•{
•//使用pictureBox控件加载并显示一个图片
•pictureBox1.Image=Image.FromFile(@"picture.JPG");
•}
•privatevoidForm2_FormClosing(objectsender,FormClosingEventArgse)
•{
•//如果少了这句,则会发生GDI资源泄漏
•//pictureBox1.Image.Dispose();
•}
用Windows任务管理器协助检测GDI泄漏
对于上面的GDI泄漏代码,可以利用Windows的任务管理器来协助检测。
方法如下:
(1)首先打开Windows任务管理器
(2)选择菜单“查看|选择列”,出现如图15.13所示界面。
确保“GDI对象”被勾选上,然后单击“确定”按钮。
(3)启动被测试程序ResourceLeak(即上面的代码例子的可执行程序),并在Windows任务管理器中定位到被测试程序的进程
(4)记下应用程序进程的当前GDI对象数,然后运行程序的各项操作,在操作过程中密切关注其GDI对象数的变化,例如,对于ResourceLeak.exe进程,当前的GDI对象数是33,如果点击button1,程序将调出第二个窗口,窗口加载了一个图片,这个过程会向系统申请一些GDI对象资源,因此查看Windows任务管理器可以看到其GDI对象数的变化
(5)这时候,把第二个窗口关闭,如果程序存在资源泄漏,则GDI对象数不会减少到33。
而且反复操作程序,调出第二个窗口再关闭,可看到GDI对象数不断地增加,这样就可判断程序存在GDI资源泄漏的现象。
利用GdiUsage检查GDI泄漏
•GdiUsage是ChristopheNasarre写的一个专门用于检查程序使用GDI资源情况的小工具
它的使用方法也很简单,具体使用方法如下:
(1)首先在上面的输入框输入需要测试的程序路径,然后按“Start”按钮启动被测试程序,程序被启动的同时,GdiUsage会显示一个“DebuggeeOutput”窗口,用于展示程序加载的DLL名称以及地址
(2)启动程序后,在GdiUsage中单击“TakeSnapshots”按钮,给当前程序使用的GDI资源情况取一个“快照”
(3)可看到当前程序使用到1个Bitmap类型的GDI对象,单击“Details”按钮,还可以看到详细的资源展示界面
(4)接着操作被测试程序(单击ResouceLeark程序的button1按钮),再单击一下“TakeSnapshots”按钮,给当前程序使用的GDI资源情况取一个“快照”
(5)可以看到当前程序使用的Bitmap对象增加到2个,Region对象增加1个。
这时关闭ResouceLeark程序的Form2窗口,再取一个快照,则发现Bitmap对象和Region对象的个数都未减少,并且如果重复这个过程,Bitmap对象的个数会不断地增加。
因此可以认为程序存在资源泄漏的现象。
GDI与GDI+
1、概述
GDI在全称是GraphicsDeviceInterface,即图形设备接口。
是图形显示与实际物理设备之间的桥梁。
GDI接口是基于函数,虽然使程序员省力不少,但是编程方式依然显得麻烦。
例如显示一张位图,我们需要进行“创建位图,读取位图文件信息,启用场景设备,调色板变化“等一系列操作。
然而有了GDI+,繁琐的步骤再次被简化。
顾名思义,GDI+就是GDI的增强版,它是微软在Windows2000以后操作系统中提供的新接口。
2、GDI+主要功能
GDI+主要提供以下三种功能:
(1)二维矢量图形:
GDI+提供了存储图形基元自身信息的类(或结构体)、存储图形基元绘制方式信息的类以及实际进行绘制的类;
(2)图像处理:
大多数图片都难以划定为直线和曲线的集合,无法使用二维矢量图形方式进行处理。
因此,GDI+为我们提供了Bitmap、Image等类,它们可用于显示、操作和保存BMP、JPG、GIF等图像格式。
(3)文字显示:
GDI+支持使用各种字体、字号和样式来显示文本。
相比于GDI,GDI+是基于C++类的对象化的应用程序接口,因此用起来更为简单。
GDI的核心是设备上下文,GDI函数都依赖于设备上下文句柄,其编程方式是基于句柄的;GDI+无需时刻依赖于句柄或设备上下文,用户只需创建一个Graphics对象,就可以用面向对象的方式调用其成员函数进行图形操作,编程方式是基于对象的。
3、GDI绘制实例
GDI在使用设备上下文绘制线条之前,必须先调用SelectObject以使笔对象和设备上下文关联。
其后,在设备上下文中绘制的所有线条均使用该笔,直到选择另一支不同的笔为止。
使用GDI画线代码如下
//TODO:
Addyourcommandhandlercodehere
CClientDCclientDC;//目标DC
CPenpen(PS_SOLID,1,RGB(0,0,255));
clientDC.SelectObject(pen.GetSafeHandle());
//开始绘制
clientDC.MoveTo(0,0)
clientDC.LineTo(rect.right,0);
clientDC.SelectObject(oldObject);
从上述代码可以看出:
在GDI编程中,几乎所有的操作都围绕设备上下文dc展开。
的确,这正是GDI编程的特点!
设备上下文是Windows使用的一种结构,所有GDI操作前都需取得特定设备的上下文,函数中的CClientDCdc(this)语句完成这一功能。
利用GDI进行图形、图像处理的一般操作步骤为:
1.取得指定窗口的DC。
2.确定使用的坐标系及映射方式。
3.进行图形、图像或文字处理。
4.释放所使用的DC。
但是,在GDI+中,只需将Pen对象直接作为参数传递给Graphics类的DrawLine等方法即可,而不必使Pen对象与Graphics对象关联。
4、GDI+绘制实例使用GDI+画线代码如下
//TODO:
Addyourcommandhandlercodehere
CClientDCclientDC(this);
//创建Graphics对象
Graphicsgraphics(clientDC);
//创建pen
PenmyPen;
myPen.SetWidth
(1);
//画X轴
myPen.SetColor(Color:
:
Blue);
graphics.DrawLine(&myPen,0,0,rect.right,0);
(1)创建Graphics对象:
Graphics对象表示GDI+绘图表面,是用于创建图形图像的对象。
(2)使用Graphics对象绘制线条和形状、呈现文本或显示与操作图像。
GDI+的相对与GDI而言,新增了一系列功能:
渐变的画刷(GradientBrushes)、基数样条函数(CardinalSplines)、持久的路径对象(PersistentPathObjects)、变形和矩阵对象(Transformations&MatrixObject)、可伸缩区域(ScalableRegions)、Alpha混合(AlphaBlending)和丰富的图像格式支持等。
下面,我们来逐个用实际代码实现GDI+的新增功能。
非托管资源造成的内存泄漏
在.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来控制,比如程序中的对象以及变量等,在.NET中,CLR提供一种独立的内存管理机制,不需要程序员在代码中显式的去释放使用到的内出资源,这种管理机制称为GC(GarbageCollection),而对于非托管资源,CLR只能跟踪非托管资源的生存期,而不能主动的去做GC,这样就需要去显式或隐式的去释放这些资源,.NET提供的Fianlize和Dispose方法分别就是隐式和显式释放只要的方法。
AQTime测试代码资源分配(ResourceProfiler)
1).新建项目File->NewProject.
2).在Setup添加LuboView.exe。
3).选择Profiler为ResourceProfiler。
4).点击Run按钮,在弹出的对话框中点击Run。
5).在EventView中可以查看到以下信息:
Event
ThreadID
Time
-
Projectrunselected,currentprofilerisResourceProfiler.
15:
18:
30:
843
Product:
AQtime;Version:
4.92.669.0
-
Workenvironment:
Hostname:
SOHU-ZGDM
OS:
MicrosoftWindowsXPServicePack25.1Build2600
Windowsdirectory:
C:
\WINDOWS;Systemdirectory:
C:
\WINDOWS\SYSTEM32
Currentuser:
Administrator
Numberofprocessors:
2
Processor:
Intel(R)Pentium(R)DCPU3.00GHz,Frequency:
~2992MHz.
Memoryinuse:
42%
TotalPhysicalMemory:
2,145,427,456bytes;AvailablePhysicalMemory:
1,238,437,888bytes;TotalVirtualMemory:
2,147,352,576bytes;AvailableVirtualMemory:
1,806,143,488bytes;VirtualMemoryInUse:
341,209,088bytes
Microsoft.NETFrameworkversion:
v2.0.50727
-
RunMod