weitom1982

向各位技术前辈学习,学习再学习.
posts - 299, comments - 79, trackbacks - 0, articles - 0
  IT博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

关注性能: 边缘剖析

Posted on 2006-03-31 13:55 高山流水 阅读(66) 评论(0)  编辑 收藏 引用
2004 年 9 月
调优的并不总是速度,有时候需要调整应用程序的其他方面,如果应用程序需要调优,要做的第一件事通常是使用剖析程序监控应用程序。但是,剖析并不总是可行的,有时候原因可能很可笑。关注性能的本期文章中, Jack 和 Kirk 讲述了他们最近经历的一件事:他们奉命剖析一个胖客户机,事实上它是如此庞大,根本没有为剖析程序留下空间。

我们从来还没有遇到过调优应用程序内存占用的问题。通常,我们看到的和内存有关的调优要求都涉及到降低垃圾收集的开销,理想情况下可以通过调整堆的大小或者改变垃圾收集算法来解决,如果不行的话,还可以采用各种技术减少内存中的对象。但是,有时候无论分配和垃圾收集的效率如何,应用程序都要占用很多的内存。

减肥中心之旅
最近,我们奉命降低一个胖客户机的内存占用。虽然“胖客户机”一词通常表示普通的 GUI 客户应用程序,而这个客户机却是几近肥胖症。这个客户机在 Windows 平台上运行,处理大小的极限是 2 GB。去掉可执行地址空间和引入各种 JNI 产品所需要的其他空间之后,该应用程序能够使用的最大堆大小大约是 1.2 GB 或者 1.3 GB。不幸的是,某些用户因为要向该应用程序灌输大量的数据,以至于占用的空间常常接近这个极限。最明显的调优方案是转移到 Unix 机器上,但是因为不切实际而被排除掉了——客户更愿意让这个应用程序减肥。

于是,我们的任务就定下来了。对这个胖客户机进行剖析,看看到底是什么占用了这些空间。然后对这些对象减肥,为以后的扩展或者更大的数据量留下空间。我们认为这事很容易。可能要花点时间,因为减少对象的数量通常不能一蹴而就,但是这么大的堆,肯定有很多赘肉能够割掉。我们这样想。

通常的过程
我们开始了通常的内存占用缩减过程:建立测试环境、规定可再现的测试、启动剖析程序、运行测试、分析数据、查找调优的机会。时间不断流逝,我们一直忙个不停……或者说我们是这样认为的。现在,我们进入了“运行测试”阶段,剖析程序趴下了。于是我们再次尝试。又死掉了。我们改变了剖析程序的配置,将开销减到最少,再次尝试。又死掉了。根本就没留下足够容纳剖析程序完全运行的 JVM 堆空间,更不用说生成任何有用的剖析数据了。而我们使用的是一种上等的商业剖析程序,一般是很可靠的,所以我们很吃惊。

试一次,再试一次
没关系,海里有数不清的鱼,现在也有数不清的剖析程序(关于剖析程序的最新评述,请参阅 Resources)。又是一天,又使用了一个剖析程序,怎么样呢?不幸的是,测试过程是惊人的相似。和一号剖析程序差不多在同一点上,二号剖析程序又让 JVM 崩溃了。和一号剖析程序一样,它甚至可以做更多的配置,重新配置,降低开销,去掉更多的数据。但它还是和一号剖析程序一样,也崩溃了。糟糕的是,三号剖析程序也没有什么不同。

巧妙的剖析程序
但是,四号剖析程序有了微妙的变化。对存活对象的快照进行内存分析(忽略对象的创建和垃圾收集,只观察某一点上存活对象的快照),在请求进行快照之前,四号剖析程序根本没有增加 JVM 的开销。成功了!我们的测试第一次在剖析程序运行的时候通过了需要拍摄快照的那个点。我们很高兴。然后我们激活了快照,于是 JVM 崩溃了。

我们又尝试了一次,但是这个剖析程序生成快照需要太多的额外空间。根本无法工作。我们又回到了起点!尽管还有半打商业剖析程序可供尝试,但结果是显然的。应该做一些横向思考了。

具有讽刺意味的是,我们的问题正在于剖析程序本身的复杂性。我们需要某种简单的东西。当然,简单并不意味着开销低,但是既然那些复杂的剖析程序令我们失望,不妨试一试。于是我们开始扫描开放源代码剖析程序。

重新开始

我们首先寻找那些看来是用于内存分析的剖析程序。一号开发源代码内存剖析程序看起来绝对简单,也许过于简单了。输出结果用处不大,只有一个类列表和每个类的对象个数。但无论如何这也算是一个不错的起点。它崩溃了。我们陷入了重走老路的担忧。二号开放源代码剖析程序甚至比一号还简单,虽然它实际上给出了更详细的信息:每个对象都有堆占转储记录,说明对象的大小和所属的类。和其他剖析程序一样,我们用较低的配置尝试,于是可以看到堆转储逐渐增大——大致就是堆的大小,然后,我们看到的是一个 1 GB 的输出文件。我们尝试了它,它击溃了虚拟机。但它确实让我们看到了部分堆转储。

在处理像这种很大的因素时,必须能够判断所需资源的数量级和要花费的时间。转存 1 GB 的文件可能要花很多时间。如果没有考虑到一个操作可能花费多长的时间,您可能错误地认为进程被挂起了,而实际上它仍然在运行,只是要花费转储 1 GB 格式化文本所需要的时间。这个开放源代码剖析程序正在工作,但是第一次测试时我们忽视了给它足够的时间。更遭的是,第一次还没有结束的时候,我们又迫使它进行第二次转储,结果造成了崩溃。所幸的是,我们认识到问题在我们自己而不是剖析程序,有了较多的认识之后,我们再次进行了尝试并取得了成功。

heapprofile 剖析程序

那么,到底哪个剖析程序成功了呢?它就是 Matthias Ernst 编写的“heapprofile”。它仅用了一页 C 代码,使用 Java Virtual Machine Profiler Interface (JVMPI) 把堆转存成最简单的格式。甚至还要自己编译,网站上(请参阅 Resources)没有提供预编译的可执行文件。这种简单性正是我们在这个问题里所需要的。没有任何开销。除了绝对必要的之外,没有使用堆或者 JNI 资源。程序运行的时候它什么也不做,当我需要堆转储的时候,它仅仅遍历一次堆,直接将每个对象的大小和类转存到一个文件,没有在内存中创建任何结构,正是这种结构让其他所有剖析程序击溃了 JVM。

当然,事情还没有完。现在我们需要分析结果数据,使用它确定应用程序所用的对象。幸运的是,输出格式很容易解析。一旦找到了造成问题的对象,我们还需要找到分配这些对象的地方。为了降低开销,我们采用重新编译这几个类的简单策略,在构造函数中放上栈跟踪程序,Jack 的著作(请参阅 Resources)中详细描述了这种技术。这种简单的技术需要在构造函数中创建(而不是抛出)异常。异常中包含分配地点的栈踪迹。然后可以将所有对象的这些栈列成表格。因为多数栈都是相同的,标识调用栈以及链接到每个栈的相关实例个数需要存储的数据并不很多(最多几千个字符串)。

简单而丑陋

这都是些简单的技术,但并没有很高的生产率。我们更愿意使用功能完备的剖析程序输出数据,尤其是因为它们提供的数据更便于分析。我们本来希望从堆的根开始,向下跟踪较大的节,直到发现大量引用堆的对象,但是我们没有选择这个方法。

和通常使用调优技术相比,这次使用的技术比较简陋。但最终我们发现了一些完全不需要的对象,使用一些类的不同实现可以完全消除它们;另一些必需的对象也可以苗条一点,或者压缩到一起,减少其空间需求。对象缩减通常都是如此,胖客户机减肥也没有一定之规。和人类一样,让 Java 应用程序节食也是很困难的事情。也和节食一样,去掉身上多余的脂肪往往比您所想的要花费更长时间。令人遗憾的是,虽然我们从这个胖客户机上刮掉了两百兆字节,但它仍然没有瘦到足以容纳“真正的”内存剖析程序的运行。

结束语

我们曾经在 Unix 讨论组看到这样一个问题 —— “Unix 大师们使用什么编辑文本?”,随后的讨论纷纷开始鼓吹 vi、Emacs 等。但毫无疑问,正确的答案应该是“Unix 大师使用任何能用的工具编辑文本。” Java 平台拥有一些非常杰出的剖析程序。但最终,调优应用程序必须分析数据,无论用何种方法,您必须拿到这些数据。

只有注册用户登录后才能发表评论。