wenmo8 发布的文章

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

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,如果您的目的是调查高内存使用率或内存泄漏,那么没有理由不这样做。

所有.NET异常都源于System.Exception以及以下成员变量。

  • Data
  • HelpLink
  • InnerException
  • Message
  • Source
  • StackTrace
  • TargetSite

除了这些基本信息之外,System.Exception通常会添加一些成员变量,它们在其中存储特定于该类型异常的信息。这一次我想谈谈一些特殊的例外情况,我们可以深入研究,以获得比乍看起来更多的信息。
更具体地说,我将讨论以下编译或加载异常:

  • System.Web.HttpCompileException
  • System.Web.HttpParseException
  • System.IO.FileNotFoundException

这里的假设是,您已经用adplus收集了一个内存转储,并且在运行!dumpheap -type时发现堆上存在上述异常之一。

System.Web.HttpCompileException

错误消息示例:

CS0246: The type or namespace name '<namespace>' could not be found (are you missing a using directive or an assembly reference?)

在server gc中,每个GC线程都将并行地在其堆上工作(这是一个简单化的视图,不一定对所有阶段都适用,但在较高的层次上,这正是并行GC的概念)。因此,仅此一项就意味着工作已经在GC线程之间被分割了。但是,由于某些阶段的GC工作只能在所有线程完成其最后一个阶段之后才能继续(例如,在所有GC线程都完成标记阶段之前,我们不能让任何GC线程从计划阶段开始,这样我们就不会错过应该标记的对象),所以我们希望每个线程上的GC工作量尽可能平衡总的暂停时间可以更短,否则,如果一个线程花费很长时间来完成这样的阶段,其他线程将等待不做任何事情。为了使工作更加平衡,我们做了很多事情。我们将继续做这样的工作来平衡更多。

当你需要调试/调查问题时,我想指出的一个非常强大的工具是你的调试器(如果你的调试器也是windbg/cdb,那就是对了,因为这就是我要用的,这也是我在本文中将要讨论的)。
对于那些对研究内存相关问题感兴趣的人来说,无论是因为你不喜欢当前应用程序的内存使用情况,还是只是想提高,学习使用调试器都是非常宝贵的。如果您还没有开始使用windbg/cdb,我强烈建议您–我保证您不会后悔的。
我以前说过使用SoS,在V4.0中添加了更多的SoS命令,其中一些与GC相关,像!AnalyzeOOM!GCWhere 和 !FindRoots。你可以在MSDN页面上看到它们。但我想谈谈一些从阅读参考资料中看不到的技巧。
当你的程序是GC时,你想知道什么是最常见的两件事吗?

1) 为什么触发GCs
2) 为什么GCs要花这么多时间?

答案分别是:

1) GC前后堆的差异
2) 那个GC的幸存者

让我详细解释一下。每一次都有自己的分配预算,我在这里解释了这一点。这是一个我们设置的值,当超过这个值时,我们想要触发一个GC。如果我们做了一个收集,发现有很多内存存留下来,我们会把这个分配预算设置得非常大,这意味着您需要为该代分配更多的内存,这样我们才能触发GC。其基本原理是,这样我们下次进行GC时就有机会回收一些相当大的空间。否则,我们将为GC做所有这些工作,而不能找到太多的死内存。
所以在GC之前,如果你运行!dumpheap,你可以看到你有什么对象;然后在GC之后你在运行!dumpheap,你会看到其中一些对象消失了。正是这些对象触发了这个GC。为什么?因为如果我们没有任何物体消失,我们就不会做GC(因为我们不会得到死内存)。
到目前为止,CLR-GC还没有压缩LOH。因此,如果你想看看LOH,你可以确切地看到内存的哪些部分消失了。下面是gen2gc前后堆范围的示例输出。在gen2gc之前(我格式化了!dumpheap,将methodtable指针替换为可读名称的)

Address              MethodTable          Size

————————————————–

00000007f7ffe1e0 System.Byte[]
2,080,00000000007f81f9ee0 System.String100,00000000007f8212580 System.Byte[]140,000After this gen2 GCforthe same heap range:



Address MethodTable Size

————————————————–

00000007f7ffe1e0 Free
2,320,000