Java性能分析、性能调优和内存分析练习

15

我将要进行一个有关Java应用程序的优化分析,性能调优、内存分析和泄漏检测等方面的研讨会,使用JProfilerEclipse Tptp这两种工具。

  • 我需要一组练习题,供参加者使用该工具来分析并发现问题:瓶颈、内存泄漏、子优代码等,并且我相信大家都有丰富的经验和实例可供分享。
  • 解决问题并实现优化的代码。
  • 通过再次进行分析工具操作来展示解决方案。
  • 最好编写单元测试以证明性能增益。

问题和解决方案不应过于复杂;最好能在几分钟内解决,最差情况下也不超过几小时。

  • 解决内存泄漏问题。
  • 优化循环。
  • 优化对象创建和管理。
  • 优化字符串操作。
  • 解决由并发性和并发性瓶颈导致的问题。

最好的练习题应该包括未优化的样本代码和解决方案代码。


1
所以您正在要求课程材料? - Thorbjørn Ravn Andersen
这些是更加精确的编程练习,我想在研讨会中使用它们,但我认为任何调整和分析Java应用程序的人都会感兴趣。 - Dan
4
如果时间紧迫,可以考虑只展示VisualVM和另外一款工具,而不是JProfiler。JProfiler虽然很方便,但如果要求使用者付费,则可能会让人们更加难以接受。实际上,其他两款工具通常已经足够发现瓶颈和死锁问题了。建议您考虑一下。 - j flemm
谢谢,我会去看看VisualVM。 - Dan
VisualVM是Sun JDK的一部分。非常有用。 - Thorbjørn Ravn Andersen
您还可以展示像Dtrace和Btrace这样的工具,因为跟踪实时系统有时对于查找泄漏或不可预测的行为是必要的。此外,您可能希望专注于其他与Java一起提供有价值洞察力的命令行工具。教授堆转储的事后分析。强调JVM的出色自我调整能力-它很少需要任何调整。展示现有的GC策略以及如何测量和调整GC。练习:创建一个带有类加载器问题(使用旧版本的commons logging)的Web应用程序(perm gen)。 - Kai Sternad
3个回答

6

我尝试寻找我在实际生活中看到的例子(可能略有改动,但基本问题都是非常真实的)。我还试图将它们聚集在相同的场景周围,这样您可以轻松地构建一个会话。

场景:您有一个耗时的函数,您想为不同的值执行多次,但是相同的值可能会再次出现(最好是在创建后不久)。一个很好而简单的例子是url-网页对,您需要下载和处理它们(对于练习,应该是模拟的)。

循环:

  • You want to check if any of a set of words pops up in the pages. Use your function in a loop, but with the same value, pseudo code:

    for (word : words) {
        checkWord(download(url))
    }
    

    One solution is quite easy, just download the page before the loop. Other solution is below.

内存泄漏:

  • 简单问题:你也可以使用某种缓存来解决问题。在最简单的情况下,您只需将结果放入(静态)映射中即可。但是如果不加以防范,它的大小将无限增长->内存泄漏。
    可能的解决方案:使用LRU映射。大多数情况下性能不会太差,但内存泄露应该消失。
  • 更棘手的问题:假设您使用WeakHashMap实现了先前的缓存,其中键是URL(而不是字符串,稍后会看到),值是包含URL、下载页面和其他内容的类的实例。您可能认为这应该没问题,但事实上并非如此:由于值(不是弱引用)具有对键(URL)的引用,因此键永远不会被清除->很好的内存泄漏。
    解决方案:从值中删除URL。
  • 与之前相同,但网址是国际化的字符串(“以节省内存,如果我们再次拥有相同的字符串”),值不引用此字符串。我没有尝试过它,但我认为它似乎也会导致泄漏,因为国际化字符串不能被GC回收。
    解决方案:不要国际化字符串,这也将导致必须遵循的建议:不要进行过早优化,因为它是万恶之源

对象创建和字符串:

  • 假设您只想显示页面的文本(~删除HTML标记)。编写一个函数,逐行执行并将其附加到增长的结果中。首先,结果应为字符串,因此附加将花费大量时间和对象分配。您可以从性能角度(为什么附加如此缓慢)和对象创建角度(为什么我们创建了许多String、StringBuffer、数组等)检测此问题。
    解决方案:使用StringBuilder作为结果。

并发:

  • 通过并行下载/过滤来加速整个过程。创建一些线程并使用它们运行代码,但在大型同步块内执行所有操作(基于缓存),只是“保护缓存免受并发问题的影响”。效果应该是您实际上只使用了一个线程,因为其他所有线程都在等待获取对缓存的锁定。
    解决方案:仅在缓存操作周围同步(例如使用`java.util.collections.synchronizedMap()`)。

  • 同步所有微小的代码片段。这应该会降低性能,可能会阻止正常的并行执行。如果您足够幸运/聪明,还可以想出死锁。 这个道理是:同步不应该是基于“它不会有害”的临时性措施,而应该是经过深思熟虑的措施。

附加练习:

在开始时填充高速缓存,之后不要进行过多的分配,但仍然存在一个小泄漏。通常这种模式不太容易被发现。您可以使用分析器的“书签”或“水印”功能,在缓存完成后立即创建。


1

不要忽略这个方法,因为它适用于任何语言和操作系统,有以下原因。这里有一个例子在这里。而且,尝试使用有I/O和重要调用深度的示例。不要只是使用像曼德布洛特这样的小型CPU绑定程序。如果您拿那个C示例,它并不太大,然后将其重编为Java,那应该会说明您的大部分观点。

让我们看看:

  • 解决内存泄漏问题。
    垃圾回收器的整个目的就是为了修补内存泄漏。但是,您仍然可能分配过多的内存,并且这会显示为某些对象中“new”的大量时间。

  • 优化循环。
    通常情况下,循环不需要进行优化,除非它们内部执行的操作非常少(并且占用了很大一部分时间)。

  • 优化对象的创建和管理。
    基本方法是:尽可能简单地保持数据结构。特别是要远离试图通过通知方式来保持数据一致性的做法,因为这些东西会失控并使调用树变得极其复杂。这是大型软件性能问题的主要原因之一。

  • 优化字符串操作。
    使用字符串生成器,但不要过于关注不占用实际执行时间的代码。

  • 并发性。
    并发性有两个目的。
    1)提高性能,但这仅在允许多个硬件同时运行时才有效。如果硬件不存在,则无济于事。它反而会造成损失。
    2)表达清晰,例如UI代码不必担心同时进行繁重的计算或网络I/O。

无论如何,都不能强调得足够,不要在证明某些东西占用了大量时间之前进行任何优化。

@Zwei:你说得对。这种方法并不能解决所有问题。有些问题可能是内存增长问题,而不是性能问题。对于这些问题,你需要使用一种工具来追踪多余对象的来源以及为什么它们没有被释放。 - Mike Dunlavey
迈克,在我的linux机器上使用jdk 6,javap显示使用'+'连接的字符串在内部被优化为使用StringBuilder类。在建议之前查看生成的字节码是值得的。 - questzen
在你证明某些东西占用了大量时间之前,不要进行任何优化。总的来说,这是非常正确的,但在这里我认为你错过了OP的意图 - 创建演示性能较差的练习,以便使用优化工具找到性能问题。 - mdma

0

我使用了JProfiler来对我们的应用程序进行分析,但它并没有提供太多帮助。然后我尝试了JHat。使用JHat无法实时查看堆。您需要获取堆转储文件,然后进行分析。使用OQL(对象查询语言)是一种很好的技术,可以找到堆泄漏问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接