📜  了解Java中的OutOfMemoryError异常

📅  最后修改于: 2020-04-04 08:00:38             🧑  作者: Mango

在Java中,所有对象都存储在堆中。使用new运算符分配它们。Java中的OutOfMemoryError异常如下所示:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

通常,当Java虚拟机由于内存不足而无法分配对象,并且垃圾回收器无法再提供更多内存时,将引发此错误。
OutOfMemoryError通常意味着您做错了什么,要么将对象保持太长时间,要么试图一次处理太多数据。有时,这表示您无法控制问题,例如缓存字符串的第三方库或部署后无法清理的应用程序服务器。有时,它与堆上的对象无关。
java.lang.OutOfMemoryError异常,也可以通过本机库的代码时,抛出本地分配不能满足(例如,如果交换空间不足)。让我们了解可能发生OutOfMemory错误的各种情况。

根本原因

为了找到原因,该异常的文本末尾包含了一条详细的消息。让我们检查所有错误。

  1. 错误1 : Java堆空间heap space:此错误是由于过度使用终结器的应用程序引起的。如果类具有finalize方法,则该类型的对象在垃圾回收时不会回收其空间。取而代之的是,在垃圾回收之后,将对象排队等待。实现方式:
    • 终结器由为终结队列提供服务的守护程序线程执行。
    • 如果终结器线程无法跟上终结器队列的速度,则Java堆可能会填满,并且会抛出此类OutOfMemoryError异常。
    • 该问题也可以像配置问题一样简单,其中指定的堆大小(或默认大小,如果未指定)对于应用程序来说是不够的。
      // Java程序展示
      // Heap error
      import java.util.*;
      public class Heap {
          static List list = new ArrayList();
      public static void main(String args[]) throws Exception
          {
              Integer[] array = new Integer[10000 * 10000];
          }
      }

      当您执行上面的代码时,您可能希望它永远运行而不会出现任何问题。结果,随着时间的流逝,随着泄漏代码的不断使用,“缓存”结果最终会占用大量Java堆空间,并且当泄漏的内存填满堆区域中的所有可用内存而Garbage Collection无法清理它,将抛出java.lang.OutOfMemoryError:Java heap space
      预防措施:监视对象待定终结中检查如何监视待定结的对象

  2. 错误2 :超出了GC开销限制:此错误表明垃圾收集器一直在运行,并且Java程序的进度非常缓慢。进行垃圾回收之后,如果Java进程花费了其大约98%的时间用于垃圾回收,并且如果它正在恢复的内存少于2%,并且到目前为止已经执行了最后5个(编译时间常数)连续垃圾集合,则抛出java.lang.OutOfMemoryError
    通常会抛出此异常,因为实时数据量几乎无法容纳到Java堆中,而新分配的可用空间却很少。
    // Java程序,展示
    // GC Overhead limit exceeded
    import java.util.*;
    public class Wrapper {
    public static void main(String args[]) throws Exception
        {
            Map m = new HashMap();
            m = System.getProperties();
            Random r = new Random();
            while (true) {
                m.put(r.nextInt(), "randomValue");
            }
        }
    }

    如果您将使用Java -Xmx100m -XX:+ UseParallelGC Wrapper运行该程序,则输出将如下所示:

    Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
        at java.lang.Integer.valueOf(Integer.Java:832)
        at Wrapper.main(error.Java:9)

    预防措施:增加堆大小并使用命令行标志-XX:-UseGCOverheadLimit将其关闭

  3. 错误3 :抛出了Permgen空间 Space error: Java内存被分成不同的区域。所有这些区域的大小,包括permgen区域,都是在JVM启动期间设置的。如果您自己不设置大小,将使用特定于平台的默认值。
    java.lang.OutOfMemoryError:PermGen space的错误表明持久代的内存区域被耗尽。
    // Java程序展示
    // Permgen Space error
    import Javassist.ClassPool;
    public class Permgen {
        static ClassPool classPool = ClassPool.getDefault();
    public static void main(String args[]) throws Exception
        {
            for (int i = 0; i < 1000000000; i++) {
                Class c = classPool.makeClass(com.saket.demo.Permgen" + i).toClass();
                System.out.println(c.getName());
            }
        }
    }

    在上面的示例代码中,代码遍历循环并在运行时生成类。Javassist库正在处理类生成的复杂性。
    运行上面的代码将继续生成新类,并将它们的定义加载到Permgen空间中,直到该空间被充分利用并且抛出java.lang.OutOfMemoryError:Permgen Space。
    预防措施:如果在应用程序启动期间由于PermGen耗尽导致OutOfMemoryError,则解决方案很简单。该应用程序仅需要更多空间来将所有类加载到PermGen区域,因此我们只需要增加其大小即可。为此,请更改您的应用程序启动配置并添加-XX:MaxPermSize 参数类似于以下示例:

    Java -XX:MaxPermSize = 512m com.saket.demo.Permgen
  4. 错误4 :Metaspace: Java类元数据分配在本机内存中。如果类元数据的元空间已用尽,则抛出java.lang.OutOfMemoryError异常,并带有详细信息MetaSpace。
    可用于类元数据的元空间的数量受参数MaxMetaSpaceSize的限制,该参数在命令行上指定。当类元数据所需的本机内存量超过MaxMetaSpaceSize时,将引发带有详细信息MetaSpace的java.lang.OutOfMemoryError异常。
    // Java展示
    // Metaspace error
    import java.util.*;
    public class Metaspace {
        static Javassist.ClassPool cp = Javassist.ClassPool.getDefault();
    public static void main(String args[]) throws Exception
        {
            for (int i = 0; i < 100000; i++) {
                Class c = cp.makeClass("com.saket.demo.Metaspace" + i).toClass();
            }
        }
    }

    这段代码将继续生成新的类,并将其定义加载到Metaspace中,直到该空间被充分利用并且抛出java.lang.OutOfMemoryError:Metaspace。当使用-XX:MaxMetaspaceSize = 64m启动程序,然后在Mac OS X上,我的Java 1.8.0_05死于大约70,000个类。
    预防措施:如果已在命令行上设置MaxMetaSpaceSize,请增加其值。MetaSpace是从与Java堆相同的地址空间分配的。减少Java堆的大小将为MetaSpace提供更多空间。如果Java堆中有多余的可用空间,这只是一个正确的权衡。

  5. 错误5 :请求的数组大小超出了VM限制:此错误表明应用程序试图分配大于堆大小的数组。例如,如果应用程序尝试分配1024 MB的数组,但最大堆大小为512 MB,则将引发OutOfMemoryError,并显示“请求的数组大小超出VM限制”。
    // Java程序展示请求的数组大小超出了VM限制
    import java.util.*;
    public class GFG {
        static List list = new ArrayList();
    public static void main(String args[]) throws Exception
        {
            Integer[] array = new Integer[10000 * 10000];
        }
    }

    java.lang.OutOfMemoryError:由于以下两种情况之一,可能会显示请求的数组大小超出VM限制:

    • 您的数组太大,最终导致其大小介于平台限制和Integer.MAX_INT之间
    • 您故意尝试分配大于2 ^ 31-1元素的数组以试验极限。
  6. 错误6 :原因是请求大小字节,交换空间不足:当从本机堆分配失败并且本机堆可能快要用尽时,就会发生此明显的异常。该错误指示失败的请求的大小(以字节为单位)以及内存请求的原因。通常,原因是报告分配失败的源模块的名称,尽管有时是实际原因。
    java.lang.OutOfMemoryError:交换空间不足错误通常是由操作系统级别的问题引起的,例如:
    • 操作系统配置的交换空间不足。
    • 系统上的另一个进程正在消耗所有内存资源。

    预防措施:引发此错误消息时,VM会调用致命错误处理机制(即,它会生成致命错误日志文件,其中包含有关崩溃时线程,进程和系统的有用信息)。在本机堆耗尽的情况下,日志中的堆内存和内存映射信息可能会很有用

  7. 错误7:stack_trace_with_native_method:每当抛出此错误消息(原因stack_trace_with_native_method)时,就会打印堆栈跟踪,其中顶部帧是本机方法,这表明本机方法遇到分配失败。该消息与上一条消息之间的区别在于,在Java本机接口(JNI)或本机方法中而不是在JVM代码中检测到分配失败。
    // Java程序展示new native thread error
    import java.util.*;
    public class GFG {
    public static void main(String args[]) throws Exception
        {
            while (true) {
                new Thread(new Runnable()
                {
                    public void run()
                    {
                        try
                        {
                            Thread.sleep(1000000000);
            }
            catch (InterruptedException e)
            {
            }
        }
                }).start();
       }
      }
    }

    确切的本机线程限制取决于平台,例如Mac OS X测试显示:
    64位Mac OS X 10.9,Java 1.7.0_45 –创建#2031线程后,JVM死亡
    预防措施:使用操作系统的本机实用程序来进一步诊断问题。