Java程序运行一段时间后变慢

19
我有一个Java程序,它是一个典型的机器学习算法,通过一些方程式更新一些参数的值:
for (int iter=0; iter<1000; iter++) {
    // 1. Create many temporary variables and do some computations                         
    // 2. Update the value for the parameters                    
}

更新参数的计算相当复杂,我必须创建许多临时对象,但它们不会在循环外被引用。循环内的代码对CPU要求较高,不涉及磁盘访问。该程序加载了一个相对较大的训练数据集,因此我将JVM分配了10G内存(-Xmx10G),这比它实际需要的要大得多(通过“top”命令或窗口任务管理器峰值为~6G)。
我在几台linux机器(centos 6,24G内存)和一台Windows机器(win7,12G)上进行了测试,都安装了SUN Hotspot JDK/JRE 1.8。我没有指定除-Xmx之外的其他JVM参数。这两台机器都专门运行我的程序。
在Windows上,我的程序运行良好:每次迭代使用的运行时间非常相似。然而,在所有centos机器上的运行时间都很奇怪。它最初正常运行,但在第7/8次迭代时显著减慢(变慢约10倍),然后在以后的每次迭代中保持减慢约10%。
我怀疑这可能是由Java的垃圾回收器引起的。因此,我使用jconsole来监视我的程序。在两台机器上,Minor GC非常频繁,这是因为程序在循环中创建了许多临时变量。此外,我使用了“jstat -gcutil $pid$ 1s”命令并捕获了统计信息:
Centos:https://www.dropbox.com/s/ioz7ai6i1h57eoo/jstat.png?dl=0 Window:https://www.dropbox.com/s/3uxb7ltbx9kpm9l/jstat-winpng.png?dl=0 [编辑] 然而,两种机器上的统计数据差异很大:
  1. 在Windows上,“S1”快速跳动在0到50之间,而在CentOS上停留在“0.00”。
  2. 在Windows上,“E”从0到100变化非常迅速。由于我每秒打印一次状态,屏幕截图没有显示其增加到100。然而,在CentOS上,“E”向100缓慢增加,然后减少到0,再次增加。

看来我的程序的奇怪行为是由Java GC引起的?我对Java性能监视器还不熟悉,也不知道如何优化GC参数设置。你有什么建议吗?非常感谢!


1
你确定需要在循环内创建大量的“临时”对象吗?如果你能将其中一些对象移出循环,并在每次迭代中重复使用相同的实例,那么就可以节省一些或者大量的垃圾(集合)。 - JimmyB
1
你们的各自机器上有多少物理内存?这可能是 CentOS 机器上的物理内存相对于 Windows 机器太少的情况,当堆增长时会发生堆交换。 - K Erlandsson
@KristofferE 这是一个CPU密集型任务(几乎使用了100%的CPU),不涉及磁盘访问。我已经使用“ps”命令监控服务器的内存使用情况,它非常稳定,机器没有任何其他重负载任务。一开始,每次迭代大约需要400秒,然后在第8次迭代时跳到2950秒,并且在以后的每次迭代中将执行时间增加10%。我观察到第一个大跳跃几乎发生在第7或第8次迭代。 - Tao Chen
@TaoChen 简单浏览了代码。不幸的是,很难得出任何结论。正如你所说,它似乎非常受CPU限制,如果GC表现良好,你就不应该看到这种行为,而它似乎确实如此。一个需要调查的问题是CPU缓存命中/未命中。这些可以极大地影响性能,并且行为可能因平台而异。我认为你需要更进一步地缩小问题范围,以便我们能够在远程帮助你更多。 - K Erlandsson
它是否随不同的JVM而改变?我们是否有能力获取完整项目的源代码(例如从Github获取Maven项目),并使其运行起来? - lschuetze
显示剩余14条评论
7个回答

1

很抱歉以回答的形式发布此内容,但我没有足够的分数来进行评论。

如果您认为这是与GC相关的问题,我建议将其更改为垃圾回收器-XX:+ UseG1GC。

我在这里找到了一个简短的解释: http://blog.takipi.com/garbage-collectors-serial-vs-parallel-vs-cms-vs-the-g1-and-whats-new-in-java-8/

您能否在性能分析下运行软件?尝试使用jprofiler、VisualVM甚至NetBeans分析器。这可能会对您有很大帮助。

我注意到您有自己的向量和矩阵封装。也许您正在浪费比必要更多的内存。但我不认为这是问题所在。

再次很抱歉无法作为评论做出贡献。(评论可能更合适)


1

我会考虑在循环外声明变量,这样内存分配只需要一次,就可以完全消除垃圾回收。


请提供需要翻译的具体内容,我将尽力为您服务。 - Syntax

1
给Java(或任何垃圾收集语言)太多的内存会对性能产生不利影响。存活(被引用的)对象在内存中变得越来越稀疏,导致更频繁地从主内存中获取。请注意,在您向我们展示的示例中,更快的Windows正在执行比Linux更快的完全GC - 但是GC周期(特别是完全GC)通常对性能不利。
如果运行训练集不需要异常长的时间,则可以尝试在不同的内存分配下进行基准测试。
一种更激进的解决方案,但应该会产生很大的影响,是通过在池中回收对象来消除(或尽可能减少)循环内的对象创建。

这听起来像是一个不错的线索,特别是因为在某一点上执行时间突然大幅跳升:这听起来像是超过了热数据缓存大小限制,因此无法将整个工作集保留在缓存中。也许系统分析器能够确认这一点。 - Andrew Janke

0
如果您的截图显示GC时间为数百毫秒,则GC可能不是此处的问题。我建议您使用分析器(Netbeans非常好)来查看锁争用和可能的IO问题。我知道您说您的程序几乎没有进行IO操作,但是在使用分析器(就像调试一样)时,您必须消除所有假设并逐步进行。

0
首先,声明变量时最好将其放在循环外面以避免垃圾回收。正如'Wagner Tsuchiya'所说,如果您对GC有疑问,请尝试运行分析器。如果您想了解一些关于GC调优的技巧,我发现这篇blogpost非常不错。

0
你可以尝试每隔几个迭代调用System.gc(),以查看性能是上升还是下降。这可能有助于缩小到一些先前答案的诊断范围。

0

根据我的经验,JAVA需要足够的内存和2个以上的CPU。否则在垃圾回收开始运行时,CPU使用率会非常高。


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