使用正确的工具进行调试
当你需要调试/调查问题时,我想指出的一个非常强大的工具是你的调试器(如果你的调试器也是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 Free2,320,000
我的应用似乎卡死了。我该怎么办?
定义“卡死”是一个很好的起点。有时又叫"挂起",英文叫"hang"。
当人们说“卡死”时,他们可能意味着各种各样的事情。当我说“卡死”时,我的意思是进程没有进展——进程中的线程要么被阻塞(例如死锁,要么因为来自其他进程的线程而没有被调度),要么正在执行代码(疯狂地)但没有做有用的工作(例如无限循环,或者长时间忙于旋转而没有做有用的工作)。前者不使用CPU,后者使用100%CPU。当UI开发人员说“卡死”时,他可能意味着“UI没有被绘制”,所以本质上它们意味着UI线程不工作——它们进程中的其他线程可能正在做大量的工作,但由于UI没有更新,它看起来是“卡死”。因此,澄清你说“卡死”的意思是第一步,这需要你查看你的进程及其线程。
如果启动任务管理器,它显示每个进程当前使用的CPU数量。如果没有看到CPU列,可以通过单击View\Select Columns并选中“CPU Usage”复选框来添加它。
请注意,如果有多个CPU,则CPU使用率最多为100。假设您有4个CPU,并且您的进程有一个线程正在运行并占用了它所能使用的所有CPU,您将看到进程CPU列是25%—因为您的进程在任何给定时间只能使用一个CPU。
进程的CPU使用率计算为属于该进程的所有线程使用的CPU使用率。线程是在CPU上运行的东西。它们由操作系统调度器调度,后者决定何时在哪个处理器上运行哪个线程。
第一个场景
如果你看到你的进程占用了0个CPU,这就可以解释为什么它挂起(在CPU保持为0的时间段内)–没有线程可以在你的进程中运行!接下来要看的是其他进程的CPU使用情况。如果您看到一个或多个其他进程占用了所有CPU,这意味着您的进程中的线程根本没有机会运行—这是因为其他进程中的线程比您进程中的线程具有更高的优先级(或由于优先级提升而暂时具有更高的优先级)。可能的原因是:
1) 有些线程被标记为低优先级,它们获取了进程中其他线程运行所需的锁。低优先级线程被其他(正常或高)优先级线程从其他进程抢占。当人们错误地使用低优先级线程执行“不重要的工作”或“不需要及时完成的工作”时,就会发生这种情况,而没有意识到几乎不可能避免对这些线程进行锁定。我听说过很多人说“但我没有锁定低优先级线程”,这不是一个有效的论据,因为你调用的API或你使用的操作系统服务可以通过锁来运行你的代码——在本机NT堆上分配可以获得锁;即使触发页面错误也会导致锁(这不是应用程序开发人员可以在代码中控制的东西)。
2) 您的进程中的线程具有正常的优先级,但其他进程具有高优先级线程-这应该相对容易诊断(除非某些进程只是坏人,否则这种情况很少发生)–您可以查看这些进程正在做什么(再次查看其线程的调用堆栈是一个很好的开始)。
第二个场景
上面挂起的场景是您的进程占用了0个CPU,而CPU被同一台机器上的其他进程占用。下一个场景是您的进程占用了0个CPU,而这个CPU几乎不被其他进程使用。
正如一位同事正确地指出的,这很可能是因为您有一个死锁。通常,调试死锁是相对简单的——您可以查看线程在等待什么,并找出其他线程持有锁。如果您使用Windows调试器包,则会有内置的调试器扩展DLL来帮助您这样做,像!locks等命令。如果您正在调试托管应用程序,SoS调试器扩展有一些命令可以帮助您–!SyncBlk,向您显示托管锁(对于clr2.0和以后的版本,还有 !Dumpheap –thinlock用于使用ThinLocks而不是SyncBlk锁定的对象)。
另一种可能是进程没有执行任何与CPU相关的活动。一个常见的活动是IO—例如,如果进程大量分页,则CPU使用率几乎为0,但它似乎挂起,因为它所需的内存从磁盘加载,速度非常慢。进程监视器是一个非常有用的工具,它可以显示进程正在做什么。昨天我机器上的一个程序周期性地暂停——非常烦人。所以我使用了进程监视器,它告诉我这个程序会定期检查我是否在另一个程序中登录到我的帐户,因为我没有,它会让我登录,做一些事情然后注销我。挂起是因为等待网络IO。所以为了让它高兴,我自己登录,然后恼人的周期性挂起消失了。
第三个场景
现在,如果你的进程确实占用了CPU,那么它也可能会挂起——正如我上次提到的,这意味着不同的人有不同的事情。如果你有一个用户界面应用程序,这可能意味着用户界面没有被绘制;如果你有一个服务器应用程序,这可能意味着你的应用程序没有处理请求。所以你必须定义挂起对你意味着什么。我将以服务器应用程序不处理请求为例。通常服务器应用程序运行在专用计算机上。因此,让我们假设这里是这样的——在一台机器上运行一台服务器,服务器可能由多个进程组成。您可以通过吞吐量来衡量服务器性能。一种情况是CPU使用率很高(甚至比平时更高),但吞吐量比平时低。
最简单的情况是无限循环,非常容易调试。你用调试器中断几次,发现一个线程占用了所有的CPU,而这个线程无法退出某个函数——于是你的无限循环就出现了。如果这个进程很明显的使用了你的CPU。如果您有多个cpu,而其他进程也在使用cpu,则会变得更加复杂。但由于它是一个无限循环,所以如果你不干涉它,它就会一直在执行,所以一旦发生,你就可以随时进行调查。
!eeheap -gc 和 !address输出的关联性
!address是一个非常强大的调试器命令。它向您显示了您的虚拟地址空间的确切分布。
如果你已经从!sos.eeheap -gc命令得到了输出,例如:
0:003> !eeheap -gc
Number of GC Heaps:1generation0 starts at 0x01245078generation1 starts at 0x0124100cgeneration2 starts at 0x01241000ephemeral segment allocation context: (0x0125a900, 0x0125b39c)
segment begin allocated size
001908c0 793fe120 7941d8a80x0001f788(128904)01240000 01241000 0125b39c 0x0001a39c(107420)
Largeobject heap starts at 0x02241000segment begin allocated size02240000 02241000 02243250 0x00002250(8784)
Total Size0x3bd74(245108)
——————————
GC Heap Size0x3bd74(245108)
在你试图找到解决办法之前,先了解你的问题
到目前为止,我还没有写过一篇博客文章,给出关于做绩效工作的哲学建议。但是最近我想也许是时候写这样一个条目了,因为我见过很多人认真地看一些性能计数器(通常是不正确的)或其他数据,并问了很多问题,比如“这个分配率太高了吗?”,“在我看来它太高了。”或者“我的size太大了,对吧?",在他们没有足够的证据来证明这样的调查和问题是合理的之前,这似乎是个大问题。
现在,如果你只是问些问题来满足你的好奇心,那就太好了。我很乐意回答您的问题,也很乐意为您指出要阅读的文档。但对于那些被要求调查与绩效相关的问题的人,尤其是在期限临近时,我的建议是“在试图找到解决方案之前,先了解问题所在”。根据证据来决定要看什么,而不是基于你对这个领域的知识的缺乏,除非你已经穷尽了你所知道的领域。在你问与GC相关的问题之前,问问你自己你是否认为GC实际上就是问题所在。如果你不能回答这个问题,那么你就不能好好利用你的时间去问与GC相关的问题。
我见过很多例子,当托管应用程序中出现问题时,人们会立即怀疑GC,而没有任何证据支持这种怀疑。然后他们开始问问题——通常是非常随机的——希望他们能在不了解问题是什么的情况下找到解决问题的方法。这不符合逻辑,是吗?所以别这样!
因此,您如何知道要解决的问题是什么,我建议您:
1、了解基础知识真的很有帮助。
什么是基础?一般来说,性能归结为两件事——内存和CPU。了解这两个方面的基本知识有助于确定要看哪一个方面。显然这需要大量的阅读和实验。我将列出一些内存基础知识,以帮助您入门:内存的一些基本原理。
每个进程都有自己独立的虚拟地址空间;同一台机器上的所有进程共享物理内存(如果有页面文件,还可以加上页文件)。在32位上,默认情况下,每个进程都有一个2GB的用户模式虚拟地址空间。
作为应用程序的作者,您使用的是虚拟地址空间—您永远不会直接操作物理内存。如果您正在编写本机代码,您通常通过某种win32堆API(crt堆或进程堆或您创建的堆)使用虚拟地址空间—这些堆API将代表您分配和释放虚拟内存;如果您正在编写托管代码,GC将代表您分配/释放虚拟内存。
虚拟地址空间可能会变得支离破碎-换句话说,地址空间中可能存在“洞”(空闲块)。当请求一个VM分配时,VM管理器需要找到一个足够大的空闲块来满足分配请求-如果只有几个空闲块的总和足够大,它将无法工作。这意味着即使你有2GB,你也不一定看到所有2GB都被使用了。
虚拟机可以处于不同的状态-空闲、保留和提交。免费很容易。有时,人们会困惑于保守和承诺之间的区别。首先,你需要认识到它们是不同的状态。Reserved是说“我想让这个内存区域供我自己使用”。在您保留了一个VM块之后,该块不能用于满足其他保留请求。此时,您还不能将任何数据存储在该内存块中—要做到这一点,您必须提交它,这意味着您必须用一些物理存储来备份数据,以便在其中存储数据。当您通过性能计数器查看内存时,请确保您看到的是正确的。如果要保留的空间或提交的空间不足,则可能会耗尽内存。
如果你有一个页面文件(默认情况下是这样),即使你的物理内存压力很低,你也可以使用它。当你的物理内存压力第一次变大,操作系统需要在物理内存中腾出空间来存储其他数据时,它会在页面文件中备份当前物理内存中的一些数据。在需要数据之前,这些数据不会被分页,这样您就可以进入物理内存负载非常低的情况下观察分页。
2、了解您的性能要求是必须的。
如果你正在编写一个服务器应用程序,很可能你想使用所有可用的内存和CPU,因为人们会完全精巧地使用机器来运行你的应用程序,那么为什么要浪费资源呢?如果你必须在另一台机器上运行的应用程序完全不同,那么你就知道如何在另一台机器上编写故事了。没有“你必须让你的应用程序尽可能少地使用内存”这样的规则。
当你认为有问题的时候,就去挖掘它,而不是去猜测可能出了什么问题。看看是谁在用你的内存。如果您认为托管堆使用了太多内存,请查看原因。使用过多内存的托管堆通常意味着您在应用程序中存活太多对象。看看那些幸存者是怎么活下来的。
又是兼容性,但确不一样
正在悠哉,突然支持的同事过来说,某个用户软件启动不了了。详细情况是:由于出现某些问题,支持同事给这个客户重装了软件,然后就启动不了了,后来把安装目录改了名字,就能运行了。虽然客户能用了,但我很不理解为什么改了个目录名就能运行呢。于是恢复现场,重现故障,不管37二十一,抓个dmp先。
用windbg打开dmp
Windows 7 Version 7601 (Service Pack 1) MP (8 procs) Free x86 compatible
Product: WinNt, suite: SingleUserTS
Machine Name:
Debug session time: Tue Sep 1 14:42:17.000 2020 (UTC + 8:00)
System Uptime: 0 days 0:09:59.840
Process Uptime: 0 days 0:00:05.000
................................................................
...............................................................
This dump file has an exception of interest stored in it.
The stored exception information can be accessed via .ecxr.
(202c.1094): Unknown exception - code c0000374 (first/second chance not available)
eax=00000000 ebx=00000000 ecx=7fffffff edx=00000000 esi=02630000 edi=0000202c
eip=7708f8d1 esp=0020e864 ebp=0020e8e8 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00200246
ntdll!NtWaitForSingleObject+0x15:
7708f8d1 83c404 add esp,4
可知,在id=1094的线程发生了c0000374(STATUS_HEAP_CORRUPTION ) 异常。
查看下栈,输入kv
0:000> kv
# ChildEBP RetAddr Args to Child
00 0020e864 77118897 00000100 00000001 00000000 ntdll!NtWaitForSingleObject+0x15 (FPO: [3,0,0])
01 0020e8e8 771189c5 0020ea88 0020ead8 00000000 ntdll!RtlReportExceptionEx+0x14b (FPO: [Non-Fpo])
02 0020e940 7713ea7e 0020ea88 0020ead8 00000000 ntdll!RtlReportException+0x86 (FPO: [Non-Fpo])
03 0020e954 7713eafb c0000374 0020e988 770e4fb4 ntdll!RtlpTerminateFailureFilter+0x14 (FPO: [Non-Fpo])
04 0020e960 770e4fb4 00000000 0020efb4 7709d100 ntdll!RtlReportCriticalFailure+0x67 (FPO: [SEH])
05 0020e974 770e4e59 00000000 00000000 00000000 ntdll!_EH4_CallFilterFunc+0x12 (FPO: [Uses EBP] [0,0,4])
06 0020e99c 770d34a1 fffffffe 0020efa4 0020ead8 ntdll!_except_handler4+0x8e (FPO: [Non-Fpo])
07 0020e9c0 770d3473 0020ea88 0020efa4 0020ead8 ntdll!ExecuteHandler2+0x26 (FPO: [Uses EBP] [5,3,1])
08 0020e9e4 770d3414 0020ea88 0020efa4 0020ead8 ntdll!ExecuteHandler+0x24 (FPO: [5,0,3])
09 0020ea70 77080133 0020ea88 0020ead8 0020ea88 ntdll!RtlDispatchException+0x127 (FPO: [Non-Fpo])
0a 0020ea70 7713eaeb 0020ea88 0020ead8 0020ea88 ntdll!KiUserExceptionDispatcher+0xf (FPO: [2,0,0]) (CONTEXT @ 0020ead8)
0b 0020efb4 7713f9f1 c0000374 77174270 0020eff8 ntdll!RtlReportCriticalFailure+0x57 (FPO: [Non-Fpo])
0c 0020efc4 7713fad1 00000002 773ec360 00000000 ntdll!RtlpReportHeapFailure+0x21 (FPO: [Non-Fpo])
0d 0020eff8 770ed97c 00000009 006b0000 007149e2 ntdll!RtlpLogHeapFailure+0xa1 (FPO: [Non-Fpo])
0e 0020f028 6dd431f7 006b0000 00000000 007149e2 ntdll!RtlFreeHeap+0x64 (FPO: [Non-Fpo])
0f 0020f03c 6dd5c6d4 007149e2 00000000 0071c978 apphelp!SdbFree+0x22 (FPO: [Non-Fpo])
10 0020f080 6dd4db99 006b1ff6 0071c978 0020f288 apphelp!SdbpBuildLayerInfo+0x3d9 (FPO: [Non-Fpo])
11 0020f144 6dd4c490 006b1ff6 0071c978 0020f288 apphelp!SdbTraceQueryResult+0xd6 (FPO: [Non-Fpo])
12 0020f260 6dd4c3be 006b1ff6 0071c978 0020f288 apphelp!SeiInit+0xcb (FPO: [Non-Fpo])
13 0020f454 770d2cae 0020f560 00070000 00000000 apphelp!SE_InstallBeforeInit+0x67 (FPO: [Non-Fpo])
14 0020f470 770d2cd0 00718b90 0020f560 00070000 ntdll!LdrpLoadShimEngine+0xdc (FPO: [Non-Fpo])
15 0020f5f8 770b9f31 0020f66c 77070000 773edad0 ntdll!LdrpInitializeProcess+0x137f (FPO: [Non-Fpo])
16 0020f648 770a9799 0020f66c 77070000 00000000 ntdll!_LdrpInitialize+0x78 (FPO: [Non-Fpo])
17 0020f658 00000000 0020f66c 77070000 00000000 ntdll!LdrInitializeThunk+0x10 (FPO: [Non-Fpo])
看来是根兼容性有关阿。栈根我之前遇到问题一样(参考<记一次因兼容性问题崩溃>)。
于是远程客户机设置兼容性,发现设置过兼容性,但不行。于是我打开注册表HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers,发现里面有很多个关于我们程序设置项,先手动删掉这些项。在双击程序,成功跑起来了。
到了这里,我又有点迷惑了,之前是需要设置兼容,为什么这次又要删掉兼容的设置项呢,看来需要研究下,因为这是个问题,我还不懂。有知道的朋友也可以直接告诉我。