Java桌面应用程序的内存分析

3
我的应用程序每次加载大约85bm到100mb的数据集。应用程序的内存限制设置为512mb,理论上这足够了。
然而,我发现如果在应用程序的单个运行中,我打开和关闭数据集5次,总内存消耗会稳步增加,直到出现内存不足错误:
 PID USER  PR NI VIRT RES SHR S %CPU %MEM TIME+   COMMAND
6882 bguiz 20 0 679m 206m 19m S 30   13.7 0:30.22 java
6882 bguiz 20 0 679m 259m 19m S 9    17.2 0:55.53 java
6882 bguiz 20 0 679m 301m 19m S 9    20.0 1:20.04 java
6882 bguiz 20 0 679m 357m 19m S 33   23.7 1:44.74 java
6882 bguiz 20 0 679m 395m 19m S 80   26.2 2:10.31 java

内存从约14%增长到约26%。看起来像是内存泄漏。

正在发生的事情是,加载的顶级数据用于填充集合,例如地图和列表,然后更详细的数据用于创建这些顶级对象的子对象,然后它们又创建子子对象。

当数据集关闭时,当前应用程序确实会尝试通过去除各种对象的集合,然后显式调用System.gc();来清除其痕迹。


无论如何,当我接手这个任务时(在我之前已经进行了几年),这就是应用程序的状态。

我需要做的是找到一种方法,找出哪些子对象和子子对象在卸载数据集后仍然相互引用,并加以纠正。
显然,这可以手动完成,但是非常非常繁琐,但我认为使用内存分析是更好的选择,这是我以前没有做过的事情。

我阅读了一些其他SO问题,询问要使用哪些内存分析工具,我选择了Netbeans IDE内置的工具,因为它似乎得到了很好的评价,并且我也在Netbeans中工作。

有人以前进行过类似的Java内存分析任务吗?有了经验教训:

  • 你会给我什么具体的建议?
  • 你发现哪些技术在解决这个问题时有用?
  • 你发现哪些资源在解决这个问题时有用?

编辑: 此应用程序是标准桌面应用程序-不是Web应用程序。


编辑:已实施的解决方案

基本上对我有用的是使用Netbeans的分析器与JHAT。

我发现Netbeans IDE内置的分析器非常出色,可以在特定的分析点创建内存转储,然后该工具能够按类过滤和排序,并深入每个实例的引用。这一切都非常好。

但是,它没有为我提供比较两个堆转储的方法。我问了一个跟进问题,看起来JHAT(作为JDK的一部分)做得很好。

Thorbjørn Ravn Andersen、Dmitry和Jason Gritman:你们的意见真的很有帮助,不幸的是我只能标记1个正确答案,但你们都得到了我的+1。


你使用哪个Java版本? - Svante
不能简单地认为将85MB的数据放入离散对象中会占用大约85MB的内存是安全的。我们尝试将由400万条记录(每条记录包含5-10个字段)组成的数据加载到内存中,并在它们之间建立引用,结果内存使用量飙升。 - jt.
@Svante 我使用Netbeans 6.7在JDK 1.6上进行开发,目标平台是Java 5和Java 6。 @jt 是的,我知道,当我加载一个85mb的数据集时,我期望内存使用量约为100mb。然而,当卸载大部分这100mb时,它们没有被解除引用(这就是问题所在)。 - bguiz
很高兴听到你解决了问题。最终泄漏的源头是什么? - Jason Gritman
@杰森,这个内存泄漏有多个来源(但只有一个触发器),迄今为止我仅使用内存分析器解决了约15%的泄漏——这些都是相当明显的:例如,一个父对象已清除了所有对其子对象的引用,但忘记了清除它们的事件监听器等。我现在正在开始使用JHAT来比较一个堆和另一个堆,以便处理大量的其他问题。 - bguiz
7个回答

3

我曾经回答过一个关于如何查找内存泄漏技术的问题,链接在https://stackoverflow.com/questions/1716597/java-memory-leak-detection-tools/1717260#1717260

如果你按照我的建议,像JProfiler这样的工具可以让你遍历对象的引用图并查看这些对象的深度大小。这可以帮助你找到仍然持有数据的任何对象。

我没有使用过Netbeans,所以无法告诉你它与我使用过的其他分析器相比如何。如果它似乎没有这个功能,你可以轻松地获得JProfiler的试用版,这应该可以让你找到内存泄漏直到解决为止。


@Jason,是的,你的答案是我在提问之前已经看过的。如果Nebans剖析器无法执行“遍历堆栈”等操作,我会考虑使用JProfiler的建议。 - bguiz
你的情况与我之前的回答有些不同,因为你知道哪些操作会导致泄漏,但你需要找到问题所在。我仍然建议使用“找出假设并测试”的方法,尝试重置或取消引用可能的罪犯,以查看它们是否是罪魁祸首。 - Jason Gritman

2

使用Java 6 JDK中的jvisualvm附加到您的程序,并查看内存使用情况。


添加相关的过滤器,以便在Visual VM中查看特定于您的应用程序包的对象计数/大小。这绝对是有用的。 - techzen

1

NetBeans分析器可能是免费工具中最好的一个。它的工作很出色,对于与分析器本身的工作没有特别提示。关于你的代码,请注意缓存某些东西的哈希映射表(特别是静态的哈希映射表)。它们往往是内存泄漏的来源。尝试使用WeakHashMap进行缓存(它不会创建指向缓存值的强引用)。


1
关于HashMap:尝试分配足够大且负载因子相对紧密的空间。每当HashMap需要增加其大小时,在此过程中它可能会暂时占用内存。 - jt.
@Dimitry,确实有大量使用HashMap的情况,感谢提醒静态。然而,这些被加载的东西不是缓存,并且在数据集关闭之前实际上需要强引用。@jt 感谢指导。 - bguiz

0
你在启动应用程序时是否更改了内存分配?例如:
java -Xmx512m -Xms128m foo.Bar

据我所知,当JVM无法快速分配内存时,也可能会发生内存不足错误。即使它在上面的示例中有512m的限制,如果JVM无法快速分配内存(超出最初的128M以上),就可能会发生内存不足错误。如果这是问题的话,从更高的-Xms值开始可能会缓解这种情况。还要注意,Xms和Xmx值只是建议,而不是硬性规定。


@jt 是的,应用程序已经设置为使用“-Xmx512m -Xms128m”,我们认为这是这个特定应用程序合理的内存使用量。在这种情况下,“Xms”值肯定不是问题,因为只有在第5次打开和关闭数据集时才会出现内存不足错误;这就是我认为这是由于内存泄漏导致的原因。 - bguiz

0

寻找用作缓存的静态集合(Map,Set,List)。


0

Eclipse Memory Analyzer也是一个很好的独立工具,用来分析堆转储文件。
您有几个选项可以创建堆的转储文件(例如,在OutOfMemoryExceptions时)。


-1
您看到的行为并不一定是内存泄漏。调用System.gc()只是提示虚拟机应该运行垃圾收集器(它不一定要运行),只要堆有足够的空闲空间,垃圾收集器通常不会运行。因此,看到进程大小增加并不意味着旧数据集不可由垃圾收集器真正回收。如果您不确定,建议生成进程的堆转储(如何取决于您使用哪个Java VM,但其文档应告诉您),并在堆转储上使用分析工具,查看是否在堆中保留了比预期更多的对象实例,并从哪里引用它们(这也将解释内存泄漏的位置)。
作为首次尝试,我可能会尝试使用自Java 1.6.0_14以来可用的新G1垃圾收集器运行程序。在正常情况下,它可能更好地提前到达可索赔实例,并且还具有能够将未使用的内存返回给操作系统的优点。其他Java垃圾收集器存在的问题是,从OS分配的内存通常直到进程退出后才会返回。

1
他说他遇到了OutOfMemory,这意味着垃圾回收无法释放任何东西(必须在抛出OOM之前尝试释放)。 - Dmitry
你说得对,我忽略了那个细节。在这种情况下,我会配置虚拟机在OutOfMemoryError时写入堆转储,并检查dump以查找剩余的指向不必要对象的引用。当VM诊断工具提供更容易找到此类错误的功能时,无需猜测问题可能出现在哪里(正如你所建议的)。 - jarnbjo
使用分析器并不是猜测,它以更易读的方式显示数据 :) - Dmitry

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