2023年1月

这并不是说它很重要,而是因为它是我的一个大麻烦,我想区分真正的内存泄漏和高内存使用率。
内存泄漏是指您使用了一些内存并且丢失了指向分配的指针,因此您无法再取消分配该内存。如果你仍然有一个指向它的指针,你的内存使用率很高,这可能与进程发生的情况一样糟糕,但仍然不同。为了简单起见,我将从现在开始对这两个问题使用术语内存泄漏…
我喜欢考虑解决性能和内存泄漏问题,比如剥洋葱皮。从最明显的一个层开始,一次去掉一个层,然后定义一个限制,确定问题已解决。
有两种类型的内存泄漏,一种是逐渐的内存泄漏(内存以大致相同的速率持续增长)和突然的内存跳跃。您可以用大致相同的方式对它们进行故障排除,但在后一种情况下,您还可以尝试确定在跳转时是否发生了异常情况,例如服务器上的极端负载等。
进程的内存空间包含一些不同类型的“对象”,如线程、管理的对象堆、托管加载程序堆、本机堆、dll和虚拟分配,因此,一个好的开始是运行带有以下计数器的性能监视器日志。

  • Process/Virtual Bytes
  •   Process/Private Bytes
  •   .net CLR Memory/# Bytes in all Heaps
  •    .net CLR Memory/% Time in GC
  •    .net CLR Memory/Large Object Heap size
  •    .net CLR Loading/Bytes in Loader Heap
  •    .net CLR Loading/Current Assemblies

主要要寻找的是私有字节的增长率是否与虚拟字节的增长率大致相同,以及所有堆中的字节是否都遵循相同的曲线。
如果私有字节持续增加,但是所有堆中的字节没有增加,那么很可能会看到本机内存泄漏(即,在COM组件或类似组件中泄漏),但如果所有堆中的字节以与私有字节相同的速率增加,则托管代码中可能会出现泄漏。
同样,如果您看到虚拟字节稳步增加,但是您的私有字节保持相当稳定,那么您的应用程序可能存在一个问题,即它保留了大量未使用的虚拟内存。
一旦进程启动并且加载了所有应用程序域,加载程序堆和当前程序集中的字节应该保持相当恒定。如果此值持续增加,则很可能存在总成泄漏。

所以现在你大概知道你在哪里泄露了,下一步就是找出原因

到目前为止,!dumpheap 是调试托管内存泄漏时最强大的命令。它将向您显示托管堆上的所有对象,并使用的不同开关你可以用你想要的任何方式显示输出。
!dumpheap是sos.dll框架安装附带的扩展(在framework目录中),如果安装了SDK,则可以在C:\Program files\ Microsoft Visual Studio.NET\2003\SDK\v1.1工具开发人员指南示例SoS中找到有关其使用的一些基本帮助。
堆上存储有两种类型的对象。以某个地方为根的对象,即应用程序中的某个对象有指向它们的指针;自上次垃圾回收后已创建或取消根目录的对象。如果您想知道泄漏在哪里,并且希望避免查看大量即将被垃圾收集的数据,那么一个好方法是运行一个压力测试,然后通过调用GC.Collect(3) ,获取一个内存转储,然后再强调一点,再次诱导GC并获取另一个内存转储并比较堆上的对象。
-stat开关(statistics)显示堆上每种类型的对象的摘要。

0:000> !dumpheap -stat
0x79c489a0 1 12System.Runtime.Remoting.Messaging.ClientContextTerminatorSink0x79bf9aec 1 12 System.IO.TextReader/NullTextReader0x79be7078 1 12System.Runtime.Remoting.Proxies.ProxyAttribute0x79bce8e0 1 12System.Runtime.InteropServices.ComVisibleAttribute0x79bce7c8 1 12System.CLSCompliantAttribute0x79bc08e0 1 12System.Empty0x0618ae68 1 12System.Web.Configuration.CustomErrorsConfigHandler0x061887f8 1 12System.Web.UI.WebControls.UnitConverter0x06180848 1 12System.Drawing.ColorConverter0x05dbfbc4 1 12System.Data.Res<… cut to save space …>

0x03f1236c 625 2,820,896System.Char[]0x04ad88f4 102,874 2,880,472System.Web.UI.ControlCollection0x0469bdf0 156,650 3,133,000System.Collections.Specialized.HybridDictionary0x04ad91bc 164,516 3,290,320System.Web.UI.Triplet0x03f134a8 7,582 3,799,704 System.Collections.Hashtable/bucket[]0x04ade5e4 47,395 4,549,920System.Web.UI.WebControls.Label0x061826bc 58,197 4,888,548System.Web.UI.DataBoundLiteralControl0x04adff44 323,119 5,169,904System.Web.UI.StateItem0x0618788c 63,437 6,089,952System.Web.UI.WebControls.TableCell0x0469c5c4 309,132 6,182,640 System.Collections.Specialized.ListDictionary/DictionaryNode0x0011cec0 305 6,240,720Free0x79ba2ee4 270,831 6,499,944System.Collections.ArrayList0x03f16d9c 222 7,703,284System.DateTime[]0x04add34c 105,502 8,018,152System.Web.UI.LiteralControl0x0615c6f4 558,019 11,160,380System.Data.DataRowView0x03f15d1c 3,783 15,447,528System.Boolean[]0x060bcc74 570,274 22,810,960System.Data.DataRow0x03f15fd4 702 50,930,472System.Decimal[]0x03f131e8 21,013 60,573,352System.Int32[]0x03f1209c 508,734 75,399,184System.Object[]0x79b94638 5,286,303 697,441,440System.String
Total
9,712,896 objects, Total size: 1,032,127,612

当你你有1000个w3wp.exe文件在eventviewer中意外停止,或者您的进程以某种奇怪的未定义方式退出,您不知道原因。
当一个进程崩溃或退出时,将触发一个称为EPR(Exit process)的特殊事件,因此使用类似于windbg.exe文件我们可以附加到进程中,等待epr被抛出,然后进行内存转储。安装windows调试工具时,会得到一个名为adplus的vbs脚本,它将为您自动执行此操作,并打印进程生命周期中发生的大多数异常的日志。
调试提示:当您在-crash模式下打开一个转储时,您将自动定位到崩溃发生时处于活动状态的线程(最有可能是可疑的线程)。如果您切换线程并想返回出错线程,请键入~列出所有线程,错误线程将被标记为一个点。
如果dump只显示进程中的一个活动线程,并且该线程是主线程,则该进程可能被外部的东西(运行状况监视、低系统内存、iisreset等)终止

不分先后顺序,以下是我们看到的支持率最高的一些:

Stack Overflow Exceptions

当为线程的堆栈分配的内存用完时,将发生堆栈溢出异常。默认情况下,它是1 MB,所以你的调用堆栈可能很深,所以大多数情况下发生这种情况是因为无限递归,也就是说,function调用FunctionB,后者再次调用FunctionB,后者再次调用FunctionB。。。没有停止条件。
不幸的是,异常处理应用程序块的错误使用是一个相当常见的模糊的无限递归情况。想象一下这个场景:你的应用程序得到一个异常,异常处理程序启动,你已经将它设置为记录到一个文件中。在记录日志时,您会得到某种类型的异常(比如访问被拒绝),并且您已经设置了异常处理程序来处理此异常。在这种情况下,您将在一个无限递归循环中处理一个异常,抛出另一个异常,处理它,抛出另一个。。。你明白要点了。这个故事的寓意是什么?不要在异常处理程序中使用异常处理程序来处理异常。
如果您运行“kb2000”(查看本机堆栈)和“!clrstack”(从sos.dll要查看托管堆栈),您可以找到递归模式以跟踪递归发生的位置/原因。

Out Of Memory Exceptions

大多数情况下,发生内存不足异常是由设计问题引起的,在设计问题中,缓存或会话作用域中存储的内存过多。如果以正确的方式使用缓存,那么缓存对于提高性能是非常有用的,也就是说,缓存的数据最多,而且缓存的时间不会超过需要的时间。在旧的ASP中,如果将对象存储在session范围内,就会出现问题,相信我,这是一种伪装,因为开发人员只在session范围内存储了最必要的项。例如,在会话范围内存储大型数据集通常会适得其反,因为您减少了网站可以处理的并发用户数,而且当内存足够大时,在缓存中进行垃圾收集和搜索所需的时间可能比从数据库中请求数据的开销要多真的需要它。
在何时应该在会话/缓存中存储内容以及何时不应该存储时,这里没有一刀切的解决方案。最好的做法是在早期阶段,确定应用程序需要能够处理的用户数,并在此基础上确定每个用户可以允许的存储量。然后对超过最大用户数的用户进行压力测试,以确保你能应付。最好是对处于会话状态的对象进行压力测试,看看性能如何。不同的用户数量不同。
在生产过程中,内存问题是很难解决的,因为它们通常需要大量的重新设计,所以在早期阶段花费一分钱可以节省很多钱。
调试提示:运行!dumpheap -type System.Web.Caching.Cache获取缓存根,然后对这些地址进行!do objsize,以了解您在缓存中为不同的应用程序存储了多少。(注意:InProc会话状态也存储在缓存中)

Unhandled exception in COM Component

如果应用程序调用本机COM组件,如果其中出现未经处理的异常,则可能会崩溃。例如,如果您引用的是已释放的内存或类似内存。kb2000将在堆栈上显示COM组件,这样您就可以缩小它的范围。

大多数情况下,方法都很短,只需知道你在哪个函数中出现了异常,就足以让你找出原因,但我们都知道,我们并不是生活在这样一个完美的世界里:我们编写的是完全模块化的应用程序,所有的东西都排列得很整齐:)
假设你在堆上发现了这个异常。。。

0:025> !dumpobj 02b7191c
Name: System.NullReferenceException
MethodTable: 7915ec4c
EEClass: 791ea18c
Size:
72(0x48) bytes (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)

Fields:
MT Field Offset Type VT Attr Value Name
790fa3e0 40000b5
4 System.String 0 instance 00000000_className79109208 40000b6 8 ...ection.MethodBase 0 instance 00000000_exceptionMethod
790fa3e0 40000b7 c System.String
0 instance 00000000_exceptionMethodString
790fa3e0 40000b8
10 System.String 0instance 02b719bc _message
79113dfc 40000b9
14 ...tions.IDictionary 0 instance 00000000_data
790fa9e8 40000ba
18 System.Exception 0 instance 00000000_innerException
790fa3e0 40000bb 1c System.String
0 instance 00000000_helpURL
790f9c18 40000bc
20 System.Object 0instance 02b71a38 _stackTrace
790fa3e0 40000bd
24 System.String 0 instance 00000000_stackTraceString
790fa3e0 40000be
28 System.String 0 instance 00000000_remoteStackTraceString
790fed1c 40000bf
34 System.Int32 0 instance 0_remoteStackIndex
790f9c18 40000c0 2c System.Object
0 instance 00000000_dynamicMethods
790fed1c 40000c1
38 System.Int32 0 instance -2147467261_HResult
790fa3e0 40000c2
30 System.String 0 instance 00000000_source
790fe160 40000c3 3c System.IntPtr
0 instance 34270984_xptrs
790fed1c 40000c4
40 System.Int32 0 instance -1073741819_xcode0:025> !printexception 02b7191c
Exception
object: 02b7191c
Exception type: System.NullReferenceException
Message: Object reference not set to an instance of an
object.
InnerException:
<none>StackTrace (generated):
SP IP Function
020AF378 029C3269 DisplayUserInfo.Page_Load(System.Object, System.EventArgs)

StackTraceString:
<none>

类比第1部分-一般内存使用

32位系统上的进程通常可以寻址2GB的虚拟地址空间。这是你必须使用的内存,与你有多少内存无关。更多的RAM有助于提高性能,因为使用更多RAM可以减少页面数量,但对于扩展2GB地址空间没有任何作用。
想象一下这个2GB地址空间是一家餐厅的占地面积。
当你分配一个对象(无论它是.net还是非.net)时,通常遵循两个步骤。您保留内存,然后提交保留区内的空间。
预订等于在餐厅预订一张桌子。我们稍后会根据你的记忆在餐厅预订。举个例子,你是一个3人的聚会。餐厅里不太可能有3个人的桌子,但是你会得到一张4人的桌子,其中你用了3个座位,浪费了一个座位。
在内存术语中,您保留的表空间称为保留内存(虚拟字节),实际使用的空间(用于3个座位)是comitted内存(私有字节)。尚未保留的楼层空间是空闲内存。
在一个不错的餐厅之夜,你的餐厅/记忆可能会像这样:蓝色区域是保留空间,红色区域表示承诺空间,白色区域是自由空间。

 

 

现在,如果有人打电话来预订3人,他们会得到的答复是餐厅已经客满,因为要让3人坐在一起的唯一方法就是让他们坐在4人桌上。即使你可以坐两张两人座的桌子,那也不太好,因为他们都想坐在一起。
类似地,当您进行内存分配时,您不会将内存保留拆分为不同的位置,它必须在一个块中分配,或者根本不分配。因此,这种情况下的内存结果将是“内存不足”,即使还有足够的空间。
观察力强的人可能也会注意到,如果我们把桌子放得更近一些,使它们完全并排在一起,你可以很容易地放进一张4人的新桌子上,但是保留的记忆区域,就像一些餐馆的桌子一样,是不能移动的。
当我们谈论内存碎片时,我们要么谈论可用但不可用的内存(因为它不够大,无法容纳新表),要么我们没有使用多少保留内存(虚拟字节和私有字节之间的差异)。

类比第2部分.NET GC

大多数时候,当你在一个应用程序中创建对象时,无论是.NET还是不使用,都使用某种内存管理器(NTeAP、C++堆、GC等),在餐馆的情况下,你可以把内存管理器想象成一个为你保留座位的女主人,并引导你到你要就座的位置。例如,如果调用Maloc,您不必提供一个地址,在这里您希望分配分配,而不是说您希望内存有一定大小,Maloc返回,OK,您将在“C++堆”区域中的表1中就位。
.NETGC更进一步,为任何可能想在这个过程中使用.NET对象的人预先预留了一个大表(比如一个64个座位的表)。当任何人创建一个.NET对象时,GC会引导他们到该表的下一个可用位置。每隔一段时间,引座员会绕着桌子走一圈,看看是否有人吃完了,请他们离开,然后把其余的人推下桌子。有些人可能会在离开前等待别人完成工作(推荐信),所以他们也可以留下来。有些人可能真的很烦人,他们会说,老兄,我有一个靠窗的座位,我根本不动(固定的物体),这意味着其他人也不能被挪到桌子的尽头。
人与人之间的任何空位都称为.NET内存碎片。
一旦64个座位的桌子被填满了,GC需要保留一个新的64个座位的桌子,如果它不能容纳新来的人,你将得到一个内存不足的异常。

好吧,这个类比够了,这是真实世界中的记忆NET应用

 

同样,红色部分是提交内存,蓝色部分是未提交的保留内存,空白部分是空闲空间。
你在内存空间末尾看到的点可能是DLL,尽管就像餐厅场景一样,有很多空白,很可能这些小红点之间的间隙都不足以容纳64MB的段,因此,下次我们填充GC段并需要一个新的GC段来容纳新对象时,将出现内存不足异常。
这些小红点(DLL)之所以这样间隔,是因为它们是在那些特定DLL的首选基地址加载的。因为很难事先知道什么是“好的”首选基址,所以对于这种类型的碎片,你做不了什么,但是你能做的是找出你实际使用的内存的去向。

关于性能计数器以及如何不使用taskmanager的注释

在整个类比过程中,我谈到了私有字节和虚拟字节,这是定义内存使用或内存泄漏时要考虑的两个最重要的性能计数器。
还有另一个称为working set的计数器,它被简化为进程中线程当前或最近接触的内存页中有多少内存,或者进程当前使用的内存中大约有多少内存在RAM中。如果您有太多分页的问题,并且同一个机器上有许多进程在争夺RAM,那么工作集计数器可能会很有趣,但是为了确定您使用了多少内存(保留或提交),它几乎没有提供帮助或没有帮助。
如果你想看到这一点,你可以创建一个winforms应用程序,分配一堆对象,然后看到工作集启动,如果你最小化应用程序,工作集就会下降。这并不意味着你已经释放了所有的记忆。它只意味着你在看一个计数器,它与决定你在内存中存储了多少东西完全无关:)然而。。。这是人们最常查看的计数器,以确定内存使用情况。。。
我知道现在你可能在想“是的,对”,你以前都没听说过这个柜台,为什么我要说这是大多数人看的柜台???答案是,因为大多数人使用任务管理器来查看进程的内存使用情况,特别是查看内存使用情况列。意外惊喜:)这实际上显示的是进程的工作集。。。
如果您想查看private bytes,这是一个更有趣的计数器,那么您可以查看task manager中标记为Virtual Memory Size(是的,这非常直观:))的列,或者更好地查看performance monitor中的processprivate bytes和processvirtual bytes,如果您的目的是调查高内存使用率或内存泄漏,那么没有理由不这样做。