减少内存流失的方法

背景

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

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

我正在搅拌很多很多年轻的生成记忆,这导致我的程序比我想象的要慢。

建立

JDK 1.6.18
Spring批次2.1.x
4核心机器16 GB内存

-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。 Minor GCs比完整的GC更好,但是它仍然会减慢我的程序(我很确定当年轻的gen集合发生时应用程序仍然会冻结,因为我看到数据库活动已经消失)。 我花费超过5%的计划时间用于较小的GC,这似乎过多。 我会说在处理这个4 GB文件的过程中,我会通过50-60GB的年轻生成内存进行流失

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

我认为与内存分析器的会话将为这个主题提供很多启示。 这样可以很好地概述创建了多少个对象,这很有趣。

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

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

Netbeans有一个很好的建立它。 我过去使用过JProfiler。 我想如果你在eclipse上敲得足够长,你可以从PPTP工具中获得相同的信息。

我有一种感觉,你花时间和精力试图优化你不应该打扰的东西。

我花费超过5%的计划时间用于较小的GC,这似乎过多。

翻转一下。 你花费的时间不到95%的时间做有用的工作。 换句话说,即使您设法优化GC在零时间运行,您可以获得的最佳效果是超过5%。

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

最重要的是整个批处理作业的挂钟时间。 并且(大约)95%的时间用于执行特定于应用程序的工作,您可能会在分析/目标优化工作中获得更多回报。 例如,您是否看过批量发送到数据库的更新?


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

这往往表明在准备要发送到数据库的内容时,大多数内存使用都发生在Oracle JDBC驱动程序中。 关于那个你很少。 我把它当成一个不可避免的开销。

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

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

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

你确定没有内存泄漏吗? 在一个消费者 – 生产者模式 – 你描述 – 很少有数据应该在Old Gen中,因为那些工作被快速处理然后“扔掉”,或者你的工作队列是否填满?

你应该用内存分析器彻底观察你的程序。

您需要对应用程序进行概要分析,以确切了解正在发生的情况。 我还会首先尝试使用JVM的人体工程学function,如建议的那样:

2.人体工程学

在J2SE 5.0中引入了一个在此称为人体工程学的function。 人体工程学的目标是通过选择,提供很少或不需要调整命令行选项来提供良好的性能

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

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

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

请参阅Java SE 6 HotSpot [tm]虚拟机垃圾收集调整指南中有关人体工程学的更详细部分。

在我看来,年轻一代不应该像老一代一样大,所以小垃圾收集保持快速。

你有很多代表相同价值的物品吗? 如果这样做,请使用简单的HashMap合并这些重复对象:

 public class MemorySavingUtils { ConcurrentHashMap knownStrings = new ConcurrentHashMap(); public String unique(String s) { return knownStrings.putIfAbsent(s, s); } public void clear() { knownStrings.clear(); } } 

使用Sun Hotspot编译器,本机String.intern()对于大量字符串来说非常慢,这就是我建议构建自己的String interner的原因。

使用此方法,可以重用旧一代的字符串,并且可以快速收集新一代的字符串。

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

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

我猜测内存限制很高,你必须在完成处理之前将文件完全读入内存。 你能考虑使用java.io.RandomAccessFile吗?