65.9K
CodeProject 正在变化。 阅读更多。
Home

最佳实践第5条:检测.NET应用程序内存泄漏

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (204投票s)

2009年9月29日

CPOL

9分钟阅读

viewsIcon

727856

downloadIcon

6381

在本文中,我们将讨论如何检测.NET应用程序的内存泄漏。

目录 

使用 finalize/dispose 模式提高垃圾回收器性能

引言

.NET应用程序中的内存泄漏一直是程序员的噩梦。内存泄漏是生产服务器面临的最大问题。生产服务器通常需要以最少的停机时间运行。内存泄漏会缓慢增长,一段时间后,它们会消耗大量内存,导致服务器崩溃。大多数时候,人们会重启系统,暂时恢复工作,然后向客户发送因停机而道歉的通知。我们将在本文中尝试解决这个问题。

避免使用任务管理器检测内存泄漏

首要任务是确认是否存在内存泄漏。许多开发人员使用Windows任务管理器来确认应用程序是否存在内存泄漏。使用任务管理器不仅具有误导性,而且无法提供有关内存泄漏位置的太多信息。

首先,让我们尝试理解任务管理器内存信息是如何具有误导性的。任务管理器显示的是工作集内存,而不是实际使用的内存。那么这是什么意思呢?这块内存是已分配的内存,而不是已使用的内存。此外,工作集中的一些内存可能被其他进程/应用程序共享。

因此,工作集内存的大小可能比实际使用的内存更大。

使用私有字节性能计数器检测内存泄漏

为了获得应用程序消耗的正确内存量,我们需要跟踪应用程序消耗的私有字节。私有字节是指不被其他应用程序共享的内存区域。为了检测应用程序消耗的私有字节,我们需要使用性能计数器。

以下是使用性能计数器跟踪应用程序中私有字节的步骤:

  • 启动有内存泄漏的应用程序并保持运行。
  • 点击“开始”->“运行”,输入“perfmon”。
  • 删除所有当前的性能计数器,选中计数器,然后按“Delete”键删除。
  • 右键单击,选择“添加计数器”,在性能对象中选择“Process”。
  • 在计数器列表中选择“Private bytes”。
  • 在实例列表中选择您要测试内存泄漏的应用程序。

如果您的应用程序的私有字节值呈稳定增长趋势,则表示存在内存泄漏问题。您可以在下图看到私有字节值如何稳定增长,从而证实了应用程序存在内存泄漏。

上图显示的是线性增长,但在实际应用中,可能需要数小时才能显示出上升趋势。为了检查内存泄漏,您需要在生产服务器上运行性能计数器数小时甚至数天,以检查是否存在真正的内存泄漏。

调查内存泄漏的三步过程

一旦确认存在内存泄漏,就该调查内存泄漏的根本原因了。我们将把解决问题的过程分为三个阶段:是什么,怎么回事,在哪里。

  • 是什么:我们首先尝试调查内存泄漏的类型,是受管内存泄漏还是非受管内存泄漏?
  • 怎么回事:内存泄漏到底是什么原因造成的?是连接对象、某个未关闭的文件句柄等吗?
  • 在哪里:哪个函数/例程或逻辑导致了内存泄漏。

内存泄漏的类型是什么?总内存 = 受管内存 + 非受管内存

在我们尝试理解泄漏的类型之前,让我们先了解.NET应用程序是如何分配内存的。.NET应用程序有两种类型的内存:受管内存和非受管内存。受管内存由垃圾回收器控制,而非受管内存则在垃圾回收器的边界之外。

所以,我们需要确保的第一件事是内存泄漏的类型:是受管泄漏还是非受管泄漏。为了检测是受管泄漏还是非受管泄漏,我们需要测量两个性能计数器。

第一个是应用程序的私有字节计数器,我们在前面的部分已经介绍过。

我们需要添加的第二个计数器是“bytes in all heaps”。因此,选择“.NET CLR memory”作为性能对象,在计数器列表中选择“Bytes in all heaps”,然后选择存在内存泄漏的应用程序。

私有字节是应用程序消耗的总内存。“Bytes in all heaps”是受管代码消耗的内存。因此,这个公式可以表示为下图所示。

非受管内存 + 所有堆中的字节 = 私有字节,所以如果我们想找出非受管内存,可以随时从私有字节中减去所有堆中的字节。

现在我们给出两个陈述:

  • 如果私有字节增加而所有堆中的字节保持不变,则表示是非受管内存泄漏。
  • 如果所有堆中的字节呈线性增加,则表示是受管内存泄漏。

下面是一个典型的非受管泄漏的截图。可以看到私有字节在增加,而堆中的字节保持不变。

下面是一个典型的受管泄漏的截图。所有堆中的字节正在增加。

内存泄漏是如何发生的?

现在我们已经回答了是什么类型的内存泄漏,是时候看看内存是如何泄漏的了。换句话说,谁导致了内存泄漏?

因此,我们通过调用Marshal.AllocHGlobal函数来注入一个非受管内存泄漏。此函数分配非受管内存,从而在应用程序中注入非受管内存泄漏。此命令会运行指定次数以造成大量的非受管泄漏。

private void timerUnManaged_Tick(object sender, EventArgs e)
{
	Marshal.AllocHGlobal(7000);
}

注入受管泄漏非常困难,因为GC会确保内存被回收。为了简单起见,我们通过创建大量画笔对象并将它们添加到作为类级别变量的列表中来模拟受管内存泄漏。这是一种模拟,而不是真正的受管泄漏。一旦应用程序关闭,这部分内存就会被回收。

private void timerManaged_Tick(object sender, EventArgs e)
{
    for (int i = 0; i < 10000; i++)
    {
        Brush obj = new SolidBrush(Color.Blue);
        objBrushes.Add(obj);
    }
}

如果您有兴趣了解受管内存中可能发生的泄漏,可以参考弱引用处理器;更多信息请访问:http://msdn.microsoft.com/en-us/library/aa970850.aspx 。

下一步是下载“debugdiag”工具,请访问:http://www.microsoft.com/en-us/download/details.aspx?id=40336 

启动调试诊断工具,选择“Memory and handle leak”,然后点击“Next”。

选择您要检测内存泄漏的进程。

最后,选择“Activate the rule now”。

现在让应用程序运行,“Debugdiag”工具将在后台监控内存问题。

完成后,点击“Start Analysis”,让工具进行分析。

您应该会收到一份详细的HTML报告,显示非受管内存是如何分配的。在我们的代码中,我们使用“AllocHGlobal”分配了大量的非受管内存,这在下面的报告中有所显示。

类型

描述

警告

mscorlib.ni.dll 负责 3.59 MB 的未完成分配。以下是消耗内存最多的前2个函数:

System.Runtime.InteropServices.Marshal.AllocHGlobal(IntPtr):负责 3.59 MB 的未完成分配。

警告

ntdll.dll 负责 270.95 KB 的未完成分配。以下是消耗内存最多的前2个函数:

ntdll!RtlpDphNormalHeapAllocate+1d:负责 263.78 KB 的未完成分配。 ntdll!RtlCreateHeap+5fc:负责 6.00 KB 的未完成分配。

画笔的受管内存泄漏通过下面的HTML报告中的“GdiPlus.dll”显示。

类型

描述

警告

GdiPlus.dll 负责 399.54 KB 的未完成分配。以下是消耗内存最多的前2个函数:

GdiPlus!GpMalloc+16:负责 399.54 KB 的未完成分配。

内存泄漏在哪里?

一旦您知道了内存泄漏的来源,就该找出是哪个逻辑导致了内存泄漏。没有自动化工具可以检测导致内存泄漏的逻辑。您需要手动检查代码,并利用“debugdiag”提供的线索来确定问题的具体位置。

例如,从报告中可以看出AllocHGlobal导致了非受管泄漏,而GDI的一个对象导致了受管泄漏。利用这些细节,我们需要深入代码来查找具体问题所在。

源代码

您可以从本文顶部下载源代码,它有助于模拟内存泄漏。

感谢,感谢,再感谢

如果我说上述文章完全是我个人的知识,那是不公平的。感谢所有在此列出的人,他们写了许多文章,使像我这样的人有一天能够从中受益。

我其他的.NET最佳实践文章

进一步阅读,请观看下面的面试准备视频和分步视频系列。

© . All rights reserved.