分类
Java

Java内存不足错误: 垃圾回收超出开销限制

译者注

原文: https://www.baeldung.com/java-gc-overhead-limit-exceeded
Demo: https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-perf

1. 概述

简单来说,当应用中的对象不再被使用时,JVM便会进行回收内存的操作,我们把这个操作称为垃圾回收,对应的原文为Garbage Collection,简称GC

GC Overhead Limit Exceeded error提示是由java.lang.OutOfMemoryError发出的,此异常代表资源已经被耗尽。

本文中我们将介绍引发java.lang.OutOfMemoryError: GC Overhead Limit Exceeded error的原因及解决方案。

2. “垃圾回收超出开销限制”的原因(GC Overhead Limit Exceeded Error)

OutOfMemoryError类是java.lang.VirtualMachineError的一个子类,当JVM 遇到资源相关的问题时,就会抛出此VirtualMachineError异常。
具体来讲,当JVM花费了太多的时间去执行垃圾回收,却只回收了很少的可用堆空间时,这个错误就会出现。

根据Java的文档, 在默认情况下JVM的设计是:如果Java进程花费了超过98%的时间去执行垃圾回收,而每次只收回了不到2%的内存时,就会抛出这个错误。
换句话讲,这就意味着我们的程序耗尽了几乎所有的可用内存,并且GC已经花费了太多了时间去清理内存,却不断的失败,不断的重复。

在这种情况下,这个应用程序的用户体验会变得非常缓慢。那些本来只需要花费毫秒级时间就能完成的操作,现在却要花费更多的时间才能完成。这是因为CPU正在使用它的计算资源来执行GC操作,因此无法执行任何其他的任务。

3. 复现这个问题

我们看一下抛出java.lang.OutOfMemoryError: GC Overhead Limit Exceeded错误的代码。

我们可以知道,例如,通过添加键值对在一个不会结束的循环(死循环)中:

public class OutOfMemoryGCLimitExceed {
    public static void addRandomDataToMap() {
        Map dataMap = new HashMap<>();
        Random r = new Random();
        while (true) {
            dataMap.put(r.nextInt(), String.valueOf(r.nextInt()));
        }
    }
}

当这个方法被调用的时候,带着如下的JVM参数运行`-Xmx100m -XX:+UseParallelGC` (这个参数的含义是:设置Java的堆大小为100M,GC收集算法为并行收集),我们就会得到一个`java.lang.OutOfMemoryError: GC Overhead Limit Exceeded`错误。
如果想要对于不同的垃圾回收策略有更深入的理解,可以学习Oracle的文章:[ Java Garbage Collection Basics tutorial.](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html " Java Garbage Collection Basics tutorial")

通过在项目的根目录运行下面的命令,我们马上就可以得到一个`java.lang.OutOfMemoryError: GC Overhead Limit Exceeded`错误:

mvn exec:exec

此外,值得注意的是,有些情况下,我们在遇到`GC Overhead Limit Exceeded`错误之前,先遇到了一个堆空间错误(`heap space error`)。

4. 解决“GC超出开销限制”错误

理想的办法就是,通过检查代码中的内存泄漏,去查找软件中的潜在问题、

下面的问题值得被解决:

  • 软件中的哪些对象占用了堆中的大量内存?
  • 这些对象在代码中的哪个位置被创建

我们也可以使用自动化图形工具(例如[JConsole](https://docs.oracle.com/en/java/javase/11/management/using-jconsole.html#GUID-77416B38-7F15-4E35-B3D1-34BFD88350B5 "JConsole")),来帮助我们检测代码中的性能问题,包括文中一直提到的java.lang.OutOfMemoryErrors。

最后一个办法是,通过改变JVM启动时的配置来增加堆内存。例如,给我们运行的Java程序设置1GB的堆内存:

java -Xmx1024m com.xyz.TheClassName

然而,这种做法并不能解决应用程序中存在的内存泄漏问题,而是会推迟这个问题的发生。因此,更可取的办法是,彻底重新评估应用程序的内存使用情况。

5. 结论

在本文中,我们学习了 java.lang.OutOfMemoryError: GC Overhead Limit Exceeded 错误,并且讨论了错误出现的原因以及如何解决它。
和其他文章一样,读者可以在Github中找到示例代码。