10

背景

我有一个 Spring 批处理程序,它读取一个文件(我正在使用的示例文件大小约为 4 GB),对文件进行少量处理,然后将其写入 Oracle 数据库。

我的程序使用 1 个线程读取文件,使用 12 个工作线程进行处理和数据库推送。

我正在搅动很多很多很多年轻一代的记忆,这导致我的程序运行得比我想象的要慢。

设置

JDK 1.6.18
Spring batch 2.1.x
4 Core Machine w 16 GB ram

-Xmx12G 
-Xms12G 
-NewRatio=1 
-XX:+UseParallelGC
-XX:+UseParallelOldGC

问题

使用这些 JVM 参数,我可以为 Tenured Generation 获得大约 5.x GB 的内存,为 Young Generation 获得大约 5.X GB 的内存。

在处理这一个文件的过程中,我的 Tenured Generation 很好。它最大可能增长到 3 GB,而且我永远不需要进行一次完整的 GC。

然而,年轻一代多次达到最大值。它上升到 5 GB 范围,然后发生并行次要 GC 并将 Young Gen 清除到使用的 500MB。次要 GC 比完整 GC 好且更好,但它仍然会大大降低我的程序速度(我很确定当发生年轻一代收集时应用程序仍然冻结,因为我看到数据库活动消失了)。我为次要 GC 冻结了超过 5% 的程序时间,这似乎过多。我想说,在处理这个 4 GB 文件的过程中,我搅动了 50-60GB 的年轻一代内存

我在我的程序中没有看到任何明显的缺陷。我试图遵守一般的 OO 原则并编写干净的 Java 代码。我试图不要无缘无故地创建对象。我正在使用线程池,并尽可能传递对象而不是创建新对象。我将开始分析应用程序,但我想知道是否有人有一些好的一般经验法则或反模式以避免导致过多的内存流失?50-60GB 的内存搅动来处理一个 4GB 的文件是我能做的最好的吗?我是否必须恢复到对象池之类的 JDK 1.2 技巧?(尽管 Brian Goetz 做了一个演讲,其中包括为什么对象池是愚蠢的,我们不需要再这样做了。我相信他比我相信自己多得多.. :))

4

7 回答 7

9

我有一种感觉,您正在花费时间和精力尝试优化您不应该打扰的东西。

我为次要 GC 冻结了超过 5% 的程序时间,这似乎过多。

把它翻过来。您将不到 95% 的程序时间用于做有用的工作。或者换一种说法,即使你设法优化了 GC 以在零时间内运行,你能得到的最好的结果是 5% 以上的改进。

如果您的应用程序有受暂停时间影响的硬时序要求,您可以考虑使用低暂停收集器。(请注意,减少暂停时间会增加整体 GC 开销……)但是对于批处理作业,GC 暂停时间不应该是相关的。

可能最重要的是整个批处理作业的挂钟时间。而(大约)95% 的时间花在做特定于应用程序的事情上,您可能会为您的分析/目标优化工作获得更多回报。例如,您是否考虑过批处理发送到数据库的更新?


所以.. 我总内存的 90% 在“oracle.sql.converter.toOracleStringWithReplacement”中的 char[] 中

这往往表明您的大部分内存使用发生在 Oracle JDBC 驱动程序中,同时准备将内容发送到数据库。你对此知之甚少。我会把它归结为不可避免的开销。

于 2010-06-20T01:17:49.937 回答
3

如果您澄清您的术语“年轻”和“受托”一代,这将非常有用,因为 Java 6 的 GC 模型略有不同:Eden、S0+S1、Old、Perm

您是否尝试过不同的垃圾收集算法?“UseConcMarkSweepGC”或“UseParNewGC”的表现如何。

并且不要忘记简单地增加可用空间不是解决方案,因为 gc 运行将花费更长的时间,将大小减小到正常值;)

你确定你没有内存泄漏吗?在您描述的消费者-生产者模式中,很少有数据应该在旧代中,因为这些工作处理得非常快然后“丢弃”,或者您的工作队列是否已满?

您应该使用内存分析器明确地观察您的程序。

于 2010-06-19T21:01:39.200 回答
2

我认为与内存分析器的会话将对这个主题有很多启发。这很好地概述了创建了多少对象,这有时会揭示。

我总是很惊讶生成了多少字符串。

对于域对象,交叉引用它们也很有启发性。如果您突然看到派生对象中的对象比源中的对象多 3 倍,那么那里发生了一些事情。

Netbeans 有一个很好的构建它。我过去使用过 JProfiler。我认为,如果您在 Eclipse 上使用的时间足够长,您可以从 PPTP 工具中获得相同的信息。

于 2010-06-19T20:51:46.793 回答
2

您需要分析您的应用程序以查看究竟发生了什么。而且我也会首先尝试使用 JVM 的人体工程学特性,如推荐的那样:

2. 人体工程学

J2SE 5.0 中引入了这里称为人体工程学的特性。人机工程学的目标是通过选择

  • 垃圾收集器,
  • 堆大小,
  • 和运行时编译器

在 JVM 启动时,而不是使用固定的默认值。该选择假定运行应用程序的机器的类别是关于应用程序特性的提示(即,大型应用程序在大型机器上运行)。除了这些选择之外,还有一种调整垃圾收集的简化方法。使用并行收集器,用户可以指定应用程序的最大暂停时间和所需吞吐量的目标。这与指定良好性能所需的堆大小相反。这旨在特别提高使用大堆的大型应用程序的性能。更一般的人体工程学在题为“5.0 Java 虚拟机中的人体工程学”的文档中进行了描述。建议在使用本文档中解释的更详细的控件之前先尝试后一文档中介绍的人体工程学

本文档中包括作为并行收集器的自适应大小策略的一部分提供的人体工程学功能。这包括指定垃圾收集性能目标的选项以及微调该性能的其他选项。

请参阅Java SE 6 HotSpot[tm] Virtual Machine Garbage Collection Tuning guide中有关人体工程学的更详细部分。

于 2010-06-19T21:57:42.447 回答
1

在我看来,年轻一代不应该和老一代一样大,这样小的垃圾收集就可以保持快速。

您是否有许多表示相同值的对象?如果这样做,请使用简单的合并这些重复对象HashMap

public class MemorySavingUtils {

    ConcurrentHashMap<String, String> knownStrings = new ConcurrentHashMap<String, String>();

    public String unique(String s) {
        return knownStrings.putIfAbsent(s, s);
    }

    public void clear() {
        knownStrings.clear();
    }
}

使用 Sun Hotspot 编译器,本机String.intern()对于大量字符串真的很慢,这就是为什么我建议构建自己的字符串内部。

使用这种方法,旧一代的字符串被重用,新生代的字符串可以快速被垃圾回收。

于 2010-06-19T20:55:41.643 回答
1

从文件中读取一行,存储为字符串并放入列表中。当列表中有 1000 个这样的字符串时,将其放入队列中以供工作线程读取。已经说过工作线程创建一个域对象,从字符串中剥离一堆值以设置字段(int、long、java.util.Date 或 String),并将域对象传递给默认的 spring batch jdbc writer

如果那是你的程序,为什么不设置更小的内存大小,比如 256MB?

于 2010-06-19T21:20:37.853 回答
1

我猜测内存限制很高,您必须在进行处理之前将文件完全读入内存。您可以考虑改用java.io.RandomAccessFile吗?

于 2010-06-19T21:25:06.490 回答