Welcome

首页 / 软件开发 / 数据结构与算法 / 执行分析还是内存分析?

执行分析还是内存分析?2014-03-29 infoq Kirk Pepperdine最近,我有一组开发者要对性能工作室中的一个问题百出的应用程序执行故障排除工作。在解决了两 个容易的问题之后,他们遇到了一个CPU运行过热的问题。这组开发者的反应和我见到的大多数面对CPU 过热问题的团队的处理方式完全一样;他们启动了一个执行分析器,希望借助它找到问题所在。这个特 殊例子中的问题是有关于应用程序是如何烧穿内存的。这种情况下,虽然一个执行分析器能够找到这些 问题,但是内存分析器将会绘制出一副更加清晰的图像。我的开发组不知道为什么遗漏了一个能够告诉 他们应该使用内存分析器的关键指标。下面就让我们通过本文中的一组相似练习看看什么时候以及为什 么使用内存分析器更好。

分析器的工作方式有三种:一是对栈顶进行取样,二是使用探针(probes)检测代码,三是联合这两 种方式。这些技术在查找经常发生的、或者占用很长时间的计算型问题时是非常好用的。正如我的小组 所经历的,执行分析器所收集到的信息通常与内存效率低下的根源有很好的相关性。但是它指向了执行 问题,有时候这样会造成混乱。

我们在清单1中发现了定义方法API findByName(String,String)的代码。这段代码的问题在很大程度 上并不在于API本身,而是在于该方法对String参数的处理方式上。该代码将两个字符串连接起来形成一 个用于在Map中查找数据的键。这种对字符串的滥用是代码中的异味,它表明这里缺少了一层抽象。正如 我们将看到的,这个缺少的抽象不仅是性能问题的根源,同时添加这层抽象还会提升代码的可读性。对 于清单1中的这种情况,缺失的抽象是一个CompositeKey<String,String>(一个包装了两个字符 串,同时实现了equals(Object)和hashCode()这两个方法的类)。

public class CustomerList {private final Map customers = new ConcurrentHashMap();public Customer addCustomer(String firstName, String lastName) {Customer person = new Customer(firstName, lastName);customers.put(firstName + lastName, person);return person;}public Customer findCustomer(String firstName, String lastName) {return (Customer) customers.get(firstName + lastName);}
清单 1. CustomerList源码

这个例子所使用的API样式还有另外一个负面影响,那就是CPU必须要写入内存的数据量限制了扩展性 。除了创建数据的额外工作之外,CPU写入内存的数据量还产生了一个强制CPU慢下来的反作用力。尽管 该场景是为了重现这个问题而人为创造的,但是在使用流行日志框架的应用程序中这是一个非常常见的 问题。也就是说,不要被愚弄了,只有字符串连接的情况也会产生错误。任何频繁操作内存的应用程序 都有可能会对内存造成压力,无论底层的数据结构是什么。

决定应用程序是否正在狂吃内存的最简单的方法是:检查垃圾收集日志。垃圾收集日志会报告每次收 集前后的堆占用情况。从当前收集之前的占用中减去前一次收集之后的占用就是两次垃圾收集期间的内 存分配量。如果我们多次记录该数据,那么我们就能获得一副很清晰的应用程序内存需求的图像。此外 ,获取需要的GC日志不仅非常容易,同时还能够知道一些对应用程序性能没有影响的边界。我使用标记 -Xloggc:gc.log和-XX:+PrintGCDetails创建了带有足够详细信息的GC日志。然后我将该GC日志文件载入 Censum(jClarity的GC日志分析工具)进行分析。

表格1.垃圾收集活动摘要