.net 框架号称永远不会发生内存泄漏,原因是其引入了内存回收的机制。但实际应用中,往往我们分配了对象但没有释放指向该对象的引用,导致对象永远无法释放。最常见的情况就是给对象添加了事件处理函数,但当不再使用该对象时却没有将该函数从对象的事件handler中减掉。另外如果分配了非托管内存,而没有手工释放,GC同样无能为力。所以当.net应用发生内存泄漏后如何跟踪应用的内存使用情况,定位到程序设计中的缺陷显得非常重要。本文将介绍通过.NET Memory Profiler来跟踪.net应用的内存泄漏,为定位.net应用内存问题提供一个解决途径。
.NET Memory Profiler是一款强大的.net 内存跟踪和优化工具。该工具目前可以对一下4种.net应用进行内存跟踪。
-
基本应用 例如winform, console application等
-
ASP.net 应用
-
WPF应用
-
Window 服务
本篇将通过对以下三种内存的跟踪来阐述如何使用该工具对基本.net应用程序进行内存的跟踪。三种内存包括:
-
托管内存
-
线程托管内存
-
非托管内存
在开始之前,先需要建立环境。
我采用.NET Memory Profiler V3.1.307 版本进行测试。安装完后需要新建一个项目,由于我们需要测
.net基本应用,所以新建项目时选择Standalone application. 点击next后,输入要测试的.net 应用的路径和参数。
然后按下 finish.项目就建立完成了。
测试程序是我编写的,编译后生成TestMemorySize.exe 这个控制台应用程序。下载地址
代码如下
主程序等待用户输入,输入m,t,u 分别是增加托管内存,创建一个自动增加托管内存的线程,增加非托管内存。
输入d,释放主线程创建的托管内存对象。
using System;
using System.Collections.Generic;
using System.Text;
namespace TestMemorySize
{
class Program
{
static void Main(string[] args)
{
MemoryInc memoryInc = new MemoryInc();
while (true)
{
long memorysize = System.Diagnostics.Process.GetCurrentProcess().PagedMemorySize64;
Console.WriteLine(string.Format("PagedMemorySize:{0}MB", memorysize / (1024*1024)));
Console.WriteLine(string.Format("ManagedMemIncTimes:{0}", memoryInc.ManagedMemIncTimes));
Console.WriteLine(string.Format("UnmanagedMemIncTimes:{0}", memoryInc.UnmanagedMemIncTimes));
String cmd = Console.ReadLine();
switch (cmd)
{
case "d":
memoryInc = new MemoryInc();
GC.Collect();
break;
case "m":
memoryInc.IncManagedMemory();
break;
case "u":
memoryInc.IncUnmanagedMemory();
break;
case "t":
MemoryLeakThread thread = new MemoryLeakThread();
break;
case "l":
break;
}
}
}
}
}
MemoryInc 是一个增加托管内存和非托管内存的类。
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
namespace TestMemorySize
{
class MemoryInc
{
int _ManagedMemIncTimes = 0;
int _UnmanagedMemIncTimes = 0;
List<byte[]> _ManagedMemory = new List<byte[]>();
LinkedList<IntPtr> _UnmanagedMemory = new LinkedList<IntPtr>();
/// <summary>
/// Managed memory increase times
/// </summary>
public int ManagedMemIncTimes
{
get
{
return _ManagedMemIncTimes;
}
}
/// <summary>
/// Unmanaged memory increase times
/// </summary>
public int UnmanagedMemIncTimes
{
get
{
return _UnmanagedMemIncTimes;
}
}
/// <summary>
/// Increase managed memory
/// </summary>
public void IncManagedMemory()
{
_ManagedMemIncTimes++;
_ManagedMemory.Add(new byte[1024 * 1024 * _ManagedMemIncTimes]);
}
/// <summary>
/// Increase unmanaged memory
/// </summary>
public void IncUnmanagedMemory()
{
_UnmanagedMemIncTimes++;
_UnmanagedMemory.AddLast(Marshal.AllocCoTaskMem(1024 * 1024 * _UnmanagedMemIncTimes));
}
}
}
MemoryLeakThread 这个线程没30秒增加1M的托管内存占用。
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace TestMemorySize
{
class MemoryLeakThread
{
Thread _Thread;
byte[] _Buf;
int _Times = 0;
private void ThreadProc()
{
while (true)
{
_Times++;
_Buf = new byte[_Times * 1024 * 1024];
Thread.Sleep(30 * 1000);
}
}
public MemoryLeakThread()
{
_Thread = new Thread(new ThreadStart(ThreadProc));
_Thread.IsBackground = true;
_Thread.Start();
}
}
}
准备就绪,下面就开始体验了。
1、托管内存的跟踪
菜单中选择Profiler->Start 启动TestMemorySize.exe,然后输入m 并回车,这是分配了1M的托管内存。
在菜单中选择Profiler->Collect Heap Shapshot. 这是就可以看到堆中的所有对象了。
从这个界面我们看到虽然列出了对象的列表,但只有类型和大小等信息,却没有对象的名称以及分配过程
信息,这样怎么定位那块内存没有被释放啊?不要着急,.NET Memory Profiler还是比较强大的,让我们继续往下
前进。
双击选中的对象后进入对象所占用的堆的详细信息
再双击选中行,这时我们就可以看到对象的名称和分配堆栈的情况了。是不是很兴奋?终于找到是哪个家伙在捣蛋了。
2、线程中创建的托管内存的跟踪
线程中创建的托管内存跟踪方法和第1节介绍的方法基本是一样的。启动TestMemorySize.exe后输入t 并回车,创建一个
吃内存的线程。下面步骤都相同了。
3、非托管内存的跟踪
要跟踪非托管内存需要做一个设置:选择菜单中view->Project Property Pages,按下图进行设置。
设置好后启动TestMemorySize.exe后输入u 并回车,创建1M的非托管内存。下面步骤相同。
非托管内存无法看到对象的名称,但可以看到内存的申请过程,这对于定位内存问题已经提供了很大的帮助。
现在我们再输入m 回车,创建1M的托管内存,然后输入d 回车,这时我们可以发现memoryInc对象申请的托管内存已经被释放掉,
但非托管内存依然存在,内存在这里泄漏了!
这个工具还可以帮助我们计算出托管对象在堆中实际占用的内存大小,这也是一个很实用的功能,我们可以发现实际的占用大小
要比我们设计的大小略大,这是因为我们设计的类及其成员都是从一些基类中继承,这些基类的数据占用了一些内存造成。