1 简介
.Net Memory Profiler(以下简称Profiler):专门针对于.NET程序,功能最全的内存分析工具,最大的特点是具有内存动态分析(Automatic Memory Analysis)功能。
2 安装
安装程序为SciTech.NET.Memory.Profiler.v4.0.114. 安装+注册机
下载地址:http://download.csdn.net/detail/wmlunge/4972685
安装完成后直接覆盖安装目录下的 memprofilerstandalone.dll 、netmemprofilerbase.dll 和 netmemprofilerconsole.exe,然后双击license.reg 文件即可完成注册。
3 使用方法
Profler可以调试4种类型的.NET程序,分别为:
l 桌面应用程序
l WPF程序
l ASP.NET程序
l .NET Service程序
对应选择软件的文件菜单如下
Profler调试共有三种方式选择:
l 启动跟踪(Profiler Application)
选定对应的调试方式,如调试桌面程序,选中Profiler Application,然后选择需要启动的执行文件,Profiler将作为宿主程序启动程序开始实时监控内存.
l 附加进程(Attach Process)
将Profiler附加到指定的进程上,此时不能实时监控内存情况,只能够收集内存镜像.
l 导入内存镜像(Import Memory Dump)
可以选择dmp为后缀的内存镜像文件,比如Windbg以及DebugDiag导出的镜像文件,此时不能实时监控内存情况,只能够收集内存镜像且不能跟踪非托管资源.
3.1 软件设置
为了加快Profiler分析内存类型实例的速度,需要设置程序的符号路径即(Symbol File Locations),进入菜单Tool->Options->Preferences->Symobl File Locations,得到弹出菜单如下图.
选中”Retrive Debug Symbols ..”选项,该选项是为了将被调试程序需要的PDB符号文件从Http://msdl.microsoft.com/download/symbols下载下来.并选定一个目录缓存原来下过的符号路径,如果有其他的分目录存放路径,则指定”Additional Symbols file locations”选项.
注:如果选择了从微软网站下载符号会影响调试程序的启动时间,建议使用本地符号集缓存
3.2 操作说明
3.2.1 启动程序
首先,选择需要调试类型,调试ZLBH桌面程序,选择 Profiler Application,选择好需要启动的程序exe文件.
如果需要设置启动参数,则设置好命令行参数以及工作目录.
选择”Next”进行收集数据的一些选项设置,一般直接按”Star”按钮开始调试程序.
3.2.2 收集数据
选择菜单栏的收集按钮,收集堆数据,第一个为收集全部堆上的数据,第二个为只收集第0代的数据.
3.2.3 重新启动和停止
调试完毕后通过停止按钮跟踪程序,通过启动按钮重新启动上一次的调试程序.
-启动
-停止
3.2.4 查看收集数据
Profiler上有6个页卡,分别为:
l Type/Resource 类型/资源页卡
l Type/Resource Details类型/资源明细页卡
l Instance Details 实例明细页卡
l Call Stacks/Methods调用堆栈页卡
l Navtive Memory 本地内存页卡
l Real-Time-实时跟踪页卡
3.2.4.1 Type/Resource 类型/资源页卡
类型/资源页卡,可以看到当前收集的内存快照的实例数/实例字节数等信息.
通过类型/资源网格的上部可以过滤出需要的信息,共有四个地方可以过滤,从左到右分别为:
l 资源类型
托管资源
非托管资源
l 警告类型
Profiler自动分析的内存问题警告类型
l 命名空间
类型的命名空间
l 类型名称
按输入过滤类型名称
类型的过滤还可以通过,”Show type/Resource”下拉框过滤出所有的已有类型.“Show hierarcical”通过命名空间分类显示类型和资源.
Live Instances 列显示当前活动的实例数
Total:总共建立的实例数
New:新建的实例数
Remved:已经销毁的实例数
Delta:New –Removed,新建和销毁数的差值.
Comparison SnapShop:另一个用来比较的内存快照来比较两个快照的差别
3.2.4.2 Type/Resource Details类型/资源明细页卡
通过在Type/Resource视图中选中某个类型则显示类型资源的明细信息,包括该类型下所有的类型实例.
左侧包括的信息包括:
l 是否新建的实例
l 实例号
l 被引用的次数
l 实例所占用的内存大小
l 实例的代信息
l 实例的子级对象所占用的内存大小
右侧包含Allocation Stacks和Shortest Root Paths,如果不是实时跟踪,则没有Allocation Stacks页卡.
l Allocation Stacks显示的是Win32调用路径
l Shortest Root Paths 显示的是从根对象到当前实例的引用路径,查看顺序从下往上,为根到实例的路径.
3.2.4.3 Instance Details 实例明细页卡
通过点击Type/Resource Details类型/资源明细页卡上的单个实例,显示这个实例的明细信息,显示的主要内容包括:
l Referenced By 被引用的关系
l References 引用的关系
l Field Value 属性的值
3.2.4.4 Call Stacks/Methods调用堆栈页卡
显示调用及方法堆栈,可以选择只包含托管代码和非托管代码
显示方法所调用的函数及被调用的函数关系,如图:
3.2.4.5 Navtive Memory 本地内存页卡
用于显示进程的本地内存信息,本地内存是被操作系统管理的内存,而不是CLR管理的内存。
3.2.4.6 Real-Time-实时跟踪页卡
如果通过Profiler Application调试程序,则能够显示出Real-Time页卡,主要内容有:
l Graph and Statistics
通过图形显示实时的内存分配情况,包括:总共实例数、存货实例数、Disposed实例数等
l Type/Resources
实时的显示出类型和资源信息,并显示最后一次gc存活的实例数以及总共的实例数。
3.2.5 自动内存分析
.NET Memory Profiler分析工具能够根据内存镜像以及实时跟踪进行自动内存问题分析,提供6个严重级别的提示,分别为严重警告、警告、轻度警告、间接警告、建议、提示。
其中对应的严重级别又会有不同的原因分类提示:
严重警告 |
|||
Potential Memory Leak |
潜在的内存泄漏 |
|
|
Disposed instance with direct EventHandler roots |
实例已Disposed但有直接的EventHandler根 |
一个Disposed的实例直接被一个EventHandler根化,这个实例只能通过代理访问到 |
|
Disposed instance with direct delegate roots |
实例已Disposed但有直接的Delegate根 |
|
|
Undisposed instances (release resource, no finalizer) |
没有被Disposed的实例 |
一个Disposable实例被GC回收,但是因为没有finalzier方法而没有正确的Dispose,从未导致外部的非托管资源没有被释放掉 |
|
警告 |
|||
Direct EventHandler roots |
被一个EventHandler直接根化 |
一个实例直接被一个EventHandler根化,需要检查这个实例以及这个EventHandler实例,是否实例被EventHandler把持 |
|
Disposed instance |
被Disposed的实例 |
一个实例虽然被Disposed但是还是标记为可到达(Reachable),需要进一步检查该实例是否是活动的(alive) |
|
Undisposed instances (release resource and remove external references) |
没有被Disposed的实例 |
一个实例被GC回收,但是没有dispose,Disposable类型的实例由于没有Dispose,导致非托管资源以及外部引用不能被移除 |
|
Undisposed instances (release resource) |
没有被Disposed的实例 |
||
Undisposed instances (remove external references) |
没有被Disposed的实例 |
||
轻度警告 |
|||
Direct delegate roots |
直接被代理所根化 |
一个Disposed的实例直接被一个Delegate根化,这个实例只能通过代理访问到 |
|
Pinned instance |
被钉住的实例 |
钉在内存中的对象因为实例不能移动,会影响GC回收效率 |
|
间接警告 |
|||
Disposed instance with indirect EventHandler roots |
已Disposed的对象被EventHandler间接根化 |
|
|
Indirect EventHandler roots |
非直接被EventHandler所根化 |
|
|
Disposed instance with indirect delegate roots |
已Disposed的对象被Deletegate间接根化 |
|
|
Indirect delegate roots |
非直接被代理所根化 |
|
|
建议 |
|||
Undisposed instances (perform action) |
没有被Disposed的实例 |
实例被回收,但是没有正确的Dispose,实例在Dispose的过程中,需要执行一些Exit或Clearup操作,包括:写数据到文件、提交或回滚事务、清除缓存、删除临时文件等 |
|
Undisposed instances (memory/resource utilization) |
实例在Dispose的过程中需要Dispose其他实例,比如:释放COM接口、suppress finalization |
||
提示 |
|||
Large instance |
大型实例对象 |
需要存放到大对象堆的实例 |
|
Undisposed instances (clear references) |
Dispose实例需要清空其他实例的引用的操作,但是没有执行 |
||
Undisposed instances (no action) |
|
||
Undisposed instances (unclassified) |
|||
4 常见内存问题
4.1 使用了非托管资源的类
非托管资源的类是指本身是被CLR管理的,而且其管理的非托管资源也可以被CLR自动回收,因为CLR只能跟踪非托管资源的生存期,但是不能主动去做GC,所以GC的时机不确定,所以在使用完后应及时释放。
例如:调用FileStream
FileStream file = new FileStream(@"c:Test.txt", FileMode.Open);
连续两次调用程序会报“文件正在使用中”的异常,如果两次调用中间调用强制回收,则不会报异常。
再例如:使用ODP.NET的OracleCommand和OracleDataReader,在Close后还需要Dispose;
OracleCommand cmd = new OracleCommand();
cmd.CommandText = sbSQL.ToString();
cmd.Connection = conn;
cmd.Parameters.Add(p1);
OracleDataReader dr = cmd.ExecuteReader();
if (dr.Read())
{
//…
dr.Close();
}
else
{
}
dr.Dispose();
cmd.Dispose();
常见的使用了非托管资源的类如下:
ApplicationContext |
Component |
ComponentDesigner |
Brush |
Container |
Context |
Cursor |
FileStream |
DataSet |
Font |
Icon |
Image |
Matrix |
Texture |
OdbcDataReader |
OleDBDataReader |
Pen |
Regex |
Socket |
StreamWriter |
Timer |
Transaction |
DataReader |
Ping |
Tooltip |
Bitmap |
SerialPort |
以上列出的类均继承了IDisposable接口,需要在使用完后调用Dispose方法释放或者使用Using语句块,比如DataTable、DataSet、DataReader、Transaction、BitMap…
4.2 Win32API及COM
指通过本地API函数与托管对象进行交互(比如:通过 P/Invoke方式调用本地DLL,DLLImport声明静态外部函数和COM Interop)所用到的非托管资源。
例如:当通过DLL Import调用 API函数GetDC函数时忘了调用ReleaseDC去释放设备句柄造成4个字节的内存泄漏。
再如:智能文档中使用的Word以及导出EXCEl功能用到的Office的COM非托管组件,在关闭时GC不能识别COM组件而造成有时候无法对COM对象进行释放,这时候可以通过以下两个InteropServices函数进行释放
l System.Runtime.InteropServices.Marshal.ReleaseComObject(comObject);
递减与指定的 COM 对象关联的指定 运行时可调用包装 (RCW) 的引用计数。
返回值为关联的 RCW 的引用计数的新值。此值通常为零,因为无论调用包装COM 对象的托管客户端有多少,RCW 仅保留对该对象的一次引用。
所以通过这个方法显式的通过CLR释放非托管 COM 对象上的所有引用。
l System.Runtime.InteropServices.Marshal.FinalReleaseComObject(comObject);
通过将 运行时可调用包装 (RCW) 的引用计数设置为 0,释放对它的所有引用。
返回值为与 comObject参数关联的 RCW 的引用计数的新值,如果释放成功,则为 0(零)。
4.3 事件造成的内存泄漏
l 当不需要使用事件时,应退订事件,为了确保安全可以在Dispose方法中退订事件.
l 当对象不再触发事件时,应该将对象设为null来移除所有的事件订阅者
4.4 动态添加生成控件造成内存泄漏
动态生成引用了非托管资源的控件后,注意一定要Dispose();
例如:
RichTextBox rtb = new RichTextBox();
frm.Controls.Add(rtb);
frm.Controls.Remove(rtb);
rtb.Dispose();
下载地址: SciTech.NET.Memory.Profiler.v4.0.114. 安装+注册机
安装完成后直接覆盖安装目录下的 memprofilerstandalone.dll 、netmemprofilerbase.dll 和 netmemprofilerconsole.exe,然后双击license.reg 文件即可完成注册。