Java.lang.OutOfMemoryError: GC overhead limit exceeded (错误:超过GC开销限制)

323
我在一个创建数百万个HashMap对象的程序中遇到了错误,每个对象包含15-20个文本条目。这些字符串必须全部收集(不分成较小的部分)后才能提交到数据库。根据Sun的说法,如果垃圾回收花费的时间太长,即如果总时间的98%以上用于垃圾回收,且只恢复了不到2%的堆,则会抛出OutOfMemoryError错误。显然,可以使用命令行向JVM传递参数来解决此问题,方法如下:
增加堆大小,通过"-Xmx1024m"(或更多);或者
完全禁用错误检查,通过"-XX:-UseGCOverheadLimit"。
第一种方法可以正常工作,而第二种方法最终会导致另一个java.lang.OutOfMemoryError错误,这次是关于堆的。因此,问题是:对于特定用例(即多个小HashMap对象),是否有编程替代方案?例如,如果我使用HashMap clear() 方法,则问题就解决了,但存储在HashMap中的数据也会消失!:-) 这个问题也在StackOverflow的相关主题中讨论过。

1
您可能需要更改算法并使用一些更有效的数据结构。您能告诉我们您正在尝试实现哪个算法,需要那么多HashMap吗? - Ankur
我正在阅读非常大的文本文件(每个文件有数十万行),我对它们没有控制权,即它们不能被分解。对于每行文本,构建一个包含少量字符串值(实际上约为10个)的HashMap,再次使用相同的数据库字段名称。理想情况下,我希望在将数据发送到数据库之前能够读取整个文件。 - PNS
1
似乎在将数据发送到数据库之前读取整个文件是非常糟糕的解决方案... 实际上,由于可用内存的非常真实的限制,这根本行不通。无论如何你为什么要这样做?"一遍又一遍地使用相同的数据库字段名称"是什么意思?作为键或值的字段名称?如果这些字段是键,则只需使用数组,其中该字段被其位置隐含表示...如果它们是值,则在将它们添加到映射之前将它们内部化。了解数据是什么将有所帮助。祝好。Keith. - corlettk
1
它们是具有恒定值的键。Intern 确实有所帮助,谢谢。 - PNS
16个回答

158

您基本上是在内存不足的情况下平稳运行该过程。以下是我能想到的一些选项:

  1. 像您提到的那样指定更多的内存,先尝试中间值,比如-Xmx512m
  2. 如果可能的话,使用较小批量的HashMap对象进行处理
  3. 如果有很多重复的字符串,请在将它们放入HashMap之前使用String.intern()处理它们
  4. 使用HashMap(int initialCapacity, float loadFactor)构造函数来优化你的情况

1
我已经接近使用HashMap的初始容量,因此程序在那里几乎是最优的。 - PNS
2
如果它可以使用更多的内存,那么有什么理由不这样做呢?如果您使用类似于“-Xms128m -Xmx1024m”这样的东西,它实际上只会增长到最大值,直到达到必要的大小。看起来是最简单的选择。 - WhiteFang34
1
是的,我想这是最快的方法。对于一些可能重复的值,我使用了intern(),问题也得到了解决。 - PNS
使用字符串intern方法一直是一个非常糟糕的想法。因为字符串池具有固定的大小,当需要时无法在运行时扩展。JVM工程师Aleksey Shipilev甚至就此主题发表了演讲("Java.lang.String Catechism")。 - G. Demecki

61

以下方法适用于我。只需添加以下代码片段:

dexOptions {
        javaMaxHeapSize "4g"
}

在你的build.gradle文件中:

android {
    compileSdkVersion 23
    buildToolsVersion '23.0.1'

    defaultConfig {
        applicationId "yourpackage"
        minSdkVersion 14
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"

        multiDexEnabled true
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    packagingOptions {

    }

    dexOptions {
        javaMaxHeapSize "4g"
    }
}

更多细节请参见:http://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.DexOptions.html - Joe Zhong
我已经尝试过这个方法,但仍然遇到了堆空间不足的问题。使用的是Android Studio 2.2.3版本。 - Coder Roadie
你的 Gradle 版本是什么? - Mina Fawzy
1
这不是一个安卓相关的问题,那么为什么要在这里发布呢? - OneCricketeer

43

@takrl:此选项的默认设置为:

java -XX:+UseConcMarkSweepGC

这意味着,默认情况下该选项是不激活的。因此,当你说你使用了选项 "+XX:UseConcMarkSweepGC" 时,我认为你使用的是以下语法:

java -XX:+UseConcMarkSweepGC

这意味着您显式地激活了此选项。 有关Java HotSpot VM选项的正确语法和默认设置,请参阅文档


在我们的情况下,使用-XX:+UseConcMarkSweepGC能够在高负载/高内存压力情况下减少“OutOfMemoryError:GC overhead limit exceeded”错误的风险,但另一方面它占用了更多的CPU,因此在正常负载情况下请求执行时间会延长5-10%。 - anre

24

顺便说一下,今天我们也遇到了同样的问题。我们通过使用这个选项来解决它:

-XX:-UseConcMarkSweepGC

显然,这修改了垃圾回收使用的策略,使问题消失了。


11

嗯...你需要采取以下措施之一:

  1. 完全重新思考算法和数据结构,使其不需要这些小HashMap。

  2. 创建一个门面,允许你根据需要将这些HashMap分页到内存中。一个简单的LRU缓存可能是最好的选择。

  3. 增加JVM可用的内存。如果必要的话,甚至购买更多的RAM可能是最快、最便宜的解决方案,如果你有管理托管这个机器的权利的话。尽管如此:我通常不喜欢“向它抛硬件”的解决方案,特别是如果在合理的时间范围内可以想出另一种算法解决方案的话。如果你在每一个问题上都一味地增加硬件,很快就会遇到收益递减的规律。

你实际上想做什么?我怀疑你的实际问题存在更好的解决方法。


请查看我上面的评论。这个用例非常简单,我正在寻找一种在处理过程中不中断的方式来处理整个大文件。谢谢! - PNS

10

使用替代的HashMap实现(Trove)。标准Java HashMap的内存开销超过12倍。 可以在此处阅读详细信息


6
<dependency> <groupId>net.sf.trove4j</groupId> <artifactId>trove4j</artifactId> <version>3.0.3</version> </dependency> 这是一段Java项目中的依赖配置,它指定了需要使用的Trove4J工具包的版本号为3.0.3。 - Jeef

9
不要在等待到达结尾时将整个结构存储在内存中。相反,将中间结果写入数据库的临时表中,而不是哈希映射表。从功能上讲,数据库表与哈希映射表相当,即都支持对数据进行键控访问,但表不受内存限制,因此在此处使用索引表而不是哈希映射表。
如果正确执行,您的算法甚至不会注意到这种变化 - 正确的做法是使用一个类来表示表格,甚至为其提供一个put(key, value)和一个get(key)方法,就像哈希映射表一样。
当中间表完成后,从中生成所需的sql语句,而不是从内存中生成。

8
并行收集器在垃圾回收过程中花费太多时间时会抛出一个OutOfMemoryError。特别地,如果超过98%的总时间用于垃圾回收,且小于2%的堆被回收,则会抛出OutOfMemoryError。该功能旨在防止应用程序因堆太小而无法长时间运行而几乎没有进展。如果需要,可以通过将选项-XX:-UseGCOverheadLimit添加到命令行来禁用此功能。

你从哪里得到这个信息的?我很感兴趣,因为它似乎非常正确。我找到了它... ---> http://www.oracle.com/technetwork/java/javase/gc-tuning-6-140523.html#par_gc.oom - Jeff Maass

5
如果您正在创建数十万个哈希映射,那么您可能使用的比实际需要的要多得多;除非您正在处理大型文件或图形,否则存储简单数据不应超出Java内存限制。
您应该尝试重新思考您的算法。在这种情况下,我可以提供更多关于该主题的帮助,但在提供有关问题上下文的更多信息之前,我无法提供任何信息。

请参见上面的评论。用例非常简单,我正在寻找一种在处理过程中不中断的方式来处理整个大文件。谢谢! - PNS

5
如果您使用的是 Java8 并且可以使用 G1 垃圾收集器,则请使用以下命令运行应用程序:
 -XX:+UseG1GC -XX:+UseStringDeduplication

这告诉G1查找相似的字符串并仅在内存中保留其中一个,其他字符串只是该内存中字符串的指针。
当您有大量重复字符串时,这很有用。这种解决方案可能适用或不适用于每个应用程序。
更多信息请参见:
https://blog.codecentric.de/en/2014/08/string-deduplication-new-feature-java-8-update-20-2/ http://java-performance.info/java-string-deduplication/

谢谢George。帮我编译Apache Camel: export MAVEN_OPTS="-Xms3000m -Xmx3000m -XX:+UseG1GC -XX:+UseStringDeduplication" - Farshid Zaker
欢迎,要注意CPU使用率,因为G1 GC对其要求稍高。 - George C

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