平衡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 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,因为人们会完全精巧地使用机器来运行你的应用程序,那么为什么要浪费资源呢?如果你必须在另一台机器上运行的应用程序完全不同,那么你就知道如何在另一台机器上编写故事了。没有“你必须让你的应用程序尽可能少地使用内存”这样的规则。
当你认为有问题的时候,就去挖掘它,而不是去猜测可能出了什么问题。看看是谁在用你的内存。如果您认为托管堆使用了太多内存,请查看原因。使用过多内存的托管堆通常意味着您在应用程序中存活太多对象。看看那些幸存者是怎么活下来的。