首页 文章

当没有足够的内存来抛出OutOfMemoryError时会发生什么?

提问于
浏览
202

我知道每个对象都需要堆内存,堆栈上的每个原语/引用都需要堆栈内存 .

当我尝试在堆上创建一个对象并且没有足够的内存来执行此操作时,JVM会在堆上创建一个java.lang.OutOfMemoryError并将其抛给我 .

所以隐含地说,这意味着JVM在启动时保留了一些内存 .

当这个保留的内存用完时会发生什么(它肯定会用完,下面的讨论),而且JVM上没有足够的内存来创建java.lang.OutOfMemoryError的实例?

它只是挂起来吗?或者他会给我一个 null ,因为没有内存 new 是OOM的一个实例?

try {
    Object o = new Object();
    // and operations which require memory (well.. that's like everything)
} catch (java.lang.OutOfMemoryError e) {
    // JVM had insufficient memory to create an instance of java.lang.OutOfMemoryError to throw to us
    // what next? hangs here, stuck forever?
    // or would the machine decide to throw us a "null" ? (since it doesn't have memory to throw us anything more useful than a null)
    e.printStackTrace(); // e.printStackTrace() requires memory too.. =X
}

==

Why couldn't the JVM reserve sufficient memory?

无论保留多少内存,如果JVM无法“回收”该内存,该内存仍有可能用完:

try {
    Object o = new Object();
} catch (java.lang.OutOfMemoryError e) {
    // JVM had 100 units of "spare memory". 1 is used to create this OOM.
    try {
        e.printStackTrace();
    } catch (java.lang.OutOfMemoryError e2) {
        // JVM had 99 units of "spare memory". 1 is used to create this OOM.
        try {
            e.printStackTrace();
        } catch (java.lang.OutOfMemoryError e3) {
            // JVM had 98 units of "spare memory". 1 is used to create this OOM.
            try {
                e.printStackTrace();
            } catch (java.lang.OutOfMemoryError e4) {
                // JVM had 97 units of "spare memory". 1 is used to create this OOM.
                try {
                    e.printStackTrace();
                } catch (java.lang.OutOfMemoryError e5) {
                    // JVM had 96 units of "spare memory". 1 is used to create this OOM.
                    try {
                        e.printStackTrace();
                    } catch (java.lang.OutOfMemoryError e6) {
                        // JVM had 95 units of "spare memory". 1 is used to create this OOM.
                        e.printStackTrace();
                        //........the JVM can't have infinite reserved memory, he's going to run out in the end
                    }
                }
            }
        }
    }
}

或者更简洁:

private void OnOOM(java.lang.OutOfMemoryError e) {
    try {
        e.printStackTrace();
    } catch (java.lang.OutOfMemoryError e2) {
        OnOOM(e2);
    }
}

11 回答

  • 64

    JVM永远不会耗尽内存 . 它预先进行堆栈的内存计算 .

    Structure of the JVM, Chapter 3,第3.5.2节规定:

    如果可以动态扩展Java虚拟机堆栈,并且尝试进行扩展,但是可以使内存不足以实现扩展,或者如果可用的内存不足以为新线程创建初始Java虚拟机堆栈,则Java虚拟机抛出OutOfMemoryError .

    对于 Heap ,第3.5.3节 .

    如果计算需要的堆数超过自动存储管理系统可用的堆,则Java虚拟机会抛出OutOfMemoryError .

    因此,它在分配对象之前预先进行计算 .


    会发生什么是JVM尝试为内存中的对象分配内存,称为永久生成区域(或PermSpace) . 如果分配失败(即使在JVM调用垃圾收集器尝试和分配可用空间之后),它也会抛出 OutOfMemoryError . 即使异常也需要内存空间,因此错误将无限期地抛出 .

    Further reading.?此外, OutOfMemoryError 可以出现在不同的JVM structure.

  • 12

    Graham Borland seems to be right:至少我的JVM显然重用了OutOfMemoryErrors . 为了测试这个,我写了一个简单的测试程序:

    class OOMTest {
        private static void test (OutOfMemoryError o) {
            try {
                for (int n = 1; true; n += n) {
                    int[] foo = new int[n];
                }
            } catch (OutOfMemoryError e) {
                if (e == o)
                    System.out.println("Got the same OutOfMemoryError twice: " + e);
                else test(e);
            }
        }
        public static void main (String[] args) {
            test(null);
        }
    }
    

    运行它会产生以下输出:

    $ javac OOMTest.java && java -Xmx10m OOMTest 
    Got the same OutOfMemoryError twice: java.lang.OutOfMemoryError: Java heap space
    

    顺便说一句,我正在运行的JVM(在Ubuntu 10.04上)是这样的:

    $ java -version
    java version "1.6.0_26"
    Java(TM) SE Runtime Environment (build 1.6.0_26-b03)
    Java HotSpot(TM) 64-Bit Server VM (build 20.1-b02, mixed mode)
    

    Edit: 我试图看看如果我使用以下程序强制JVM完全从内存中运行会发生什么:

    class OOMTest2 {
        private static void test (int n) {
            int[] foo;
            try {
                foo = new int[n];
                test(n * 2);
            }
            catch (OutOfMemoryError e) {
                test((n+1) / 2);
            }
        }
        public static void main (String[] args) {
            test(1);
        }
    }
    

    事实证明,它似乎永远循环 . 但是,奇怪的是,尝试使用Ctrl C终止程序不起作用,但只提供以下消息:

    Java HotSpot(TM) 64-Bit Server VM warning: Exception java.lang.OutOfMemoryError occurred dispatching signal SIGINT to handler- the VM may need to be forcibly terminated

  • 22

    大多数运行时环境将在启动时预分配,或以其他方式保留足够的内存来处理内存不足情况 . 我想大多数理智的JVM实现都会这样做 .

  • 4

    上次我使用Java并使用调试器时,堆检查器显示JVM在启动时分配了一个OutOfMemoryError实例 . 换句话说,它在程序有机会开始消耗之前分配对象,更不用说耗尽内存了 .

  • 6

    从JVM规范,第3.5.2节:

    如果可以动态扩展Java虚拟机堆栈,并且尝试进行扩展,但是可以使内存不足以实现扩展,或者如果可用的内存不足以为新线程创建初始Java虚拟机堆栈,则Java虚拟机抛出OutOfMemoryError .

    每个Java虚拟机都必须保证它会抛出 OutOfMemoryError . 这意味着,即使没有剩余堆空间,它也必须能够创建 OutOfMemoryError 的实例(或者事先创建) .

    虽然它没有必要保证,但是有足够的内存来捕获它并打印出漂亮的堆栈跟踪...

    Addition

    您添加了一些代码来表明,如果必须抛出多个 OutOfMemoryError ,JVM可能会耗尽堆空间 . 但是这样的实施会违反上述要求 .

    不要求 OutOfMemoryError 的抛出实例是唯一的或按需创建的 . JVM可以在启动期间准备一个 OutOfMemoryError 的实例,并在堆空间用完时抛出它 - 在正常环境中这是一次 . 换句话说:我们看到的 OutOfMemoryError 实例可能是一个单例 .

  • 144

    有趣的问题:-) . 虽然其他人对理论方面给出了很好的解释,但我决定尝试一下 . 这是在Oracle JDK 1.6.0_26,Windows 7 64位上 .

    Test setup

    我写了一个简单的程序来耗尽内存(见下文) .

    该程序只是创建一个静态 java.util.List ,并保持填充新的字符串,直到抛出OOM . 然后它捕获它并继续填充无限循环(可怜的JVM ......) .

    Test result

    从输出中可以看出,抛出OOME的前四次,它带有堆栈跟踪 . 之后,随后如果调用 printStackTrace() ,OOME仅打印 java.lang.OutOfMemoryError: Java heap space .

    显然,如果可以的话,JVM会努力打印堆栈跟踪,但如果内存非常严格,它就会省略跟踪,就像其他答案所暗示的那样 .

    同样有趣的是OOME的哈希码 . 请注意,前几个OOME都有不同的哈希值 . 一旦JVM开始省略堆栈跟踪,散列总是相同的 . 这表明JVM将尽可能长时间地使用新的(预分配的?)OOME实例,但是如果推送推进,它将只重用相同的实例,而不是没有任何东西可以抛出 .

    Output

    注意:我截断了一些堆栈跟踪以使输出更易于阅读("[...]") .

    iteration 0
    iteration 100000
    iteration 200000
    iteration 300000
    iteration 400000
    iteration 500000
    iteration 600000
    iteration 700000
    iteration 800000
    iteration 900000
    iteration 1000000
    iteration 1100000
    iteration 1200000
    iteration 1300000
    iteration 1400000
    iteration 1500000
    Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1069480624
    Keep on trying...
    java.lang.OutOfMemoryError: Java heap space
        at java.util.Arrays.copyOf(Unknown Source)
        at java.util.Arrays.copyOf(Unknown Source)
        at java.util.ArrayList.ensureCapacity(Unknown Source)
        at java.util.ArrayList.add(Unknown Source)
        at testsl.Div.gobbleUpMemory(Div.java:23)
        at testsl.Div.exhaustMemory(Div.java:12)
        at testsl.Div.main(Div.java:7)
    java.lang.OutOfMemoryError: Java heap space
        at java.util.Arrays.copyOf(Unknown Source)
    [...]
    Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 616699029
    Keep on trying...
    java.lang.OutOfMemoryError: Java heap space
        at java.util.Arrays.copyOf(Unknown Source)
    [...]
    Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 2136955031
    Keep on trying...
    java.lang.OutOfMemoryError: Java heap space
        at java.util.Arrays.copyOf(Unknown Source)
    [...]
    Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1535562945
    Keep on trying...
    java.lang.OutOfMemoryError: Java heap space
    Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1734048134
    Keep on trying...
    Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1734048134
    Keep on trying...
    java.lang.OutOfMemoryError: Java heap space
    Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1734048134
    Keep on trying...
    [...]
    

    The program

    public class Div{
        static java.util.List<String> list = new java.util.ArrayList<String>();
    
        public static void main(String[] args) {
            exhaustMemory();
        }
    
        private static void exhaustMemory() {
            try {
                gobbleUpMemory();
            } catch (OutOfMemoryError e) {
                System.out.println("Ouch: " + e+"; hash: "+e.hashCode());
                e.printStackTrace();
                System.out.println("Keep on trying...");
                exhaustMemory();
            }
        }
    
        private static void gobbleUpMemory() {
            for (int i = 0; i < 10000000; i++) {
                list.add(new String("some random long string; use constructor to force new instance"));
                if (i % 10000000== 0) {
                    System.out.println("iteration "+i);
                }
            }
    
        }
    }
    
  • 2

    我很确定,JVM将确保它至少有足够的内存来在内存不足之前抛出异常 .

  • 41

    表示试图违反托管内存环境边界的异常由所述环境的运行时处理,在本例中为JVM . JVM是它自己的进程,运行应用程序的IL . 如果程序尝试进行调用以扩展调用堆栈超出限制,或者分配比JVM可以保留的内存更多的内存,则运行时本身将注入异常,这将导致调用堆栈被展开 . 无论程序当前需要多少内存,或者调用堆栈的深度如何,JVM都会在其自己的进程边界内分配足够的内存来创建所述异常并将其注入到代码中 .

  • 4

    您似乎混淆了JVM保留的虚拟内存,其中JVM运行Java程序与主机操作系统的本机内存,其中JVM作为本机进程运行 . 计算机上的JVM在由OS管理的内存中运行,而不是在JVM保留用于运行Java程序的内存中运行 .

    进一步阅读:

    作为最后一点,为了打印堆栈跟踪而尝试 catch (及其后代类)可能不会给你任何有用的信息 . 您想要堆转储 .

  • 11

    为了进一步澄清@Graham Borland的答案,功能上,JVM在启动时执行此操作:

    private static final OutOfMemoryError OOME = new OutOfMemoryError();
    

    稍后,JVM执行以下Java字节码之一:'new','anewarray'或'multianewarray' . 此指令使JVM在内存不足情况下执行许多步骤:

    • 调用本机函数,比如 allocate() . allocate() 尝试为某个特定类或数组的新实例分配内存 .

    • 该分配请求失败,因此JVM调用另一个本机函数,比如 doGC() ,它尝试进行垃圾回收 .

    • 当该函数返回时, allocate() 再次尝试为该实例分配内存 .

    • 如果失败(*),则在allocate()中的JVM只执行 throw OOME; ,指的是它在启动时实例化的OOME . 请注意,它不必分配OOME,它只是引用它 .

    显然,这些不是文字步骤;它们在实现中会从JVM变为JVM,但这是高级想法 .

    (*)在失败之前,这里发生了大量的工作 . JVM将尝试清除SoftReference对象,在使用分代收集器时尝试直接分配到tenured generation,以及可能的其他内容,如finalization .

  • 4

    说JVM将预先分配 OutOfMemoryErrors 的答案确实是正确的 .
    除了通过激发内存不足情况进行测试之外,我们还可以检查任何JVM的堆(我使用了一个只是睡眠的小程序,使用Oracle的Hotspot JVM从Java 8更新31运行它) .

    使用 jmap ,我们看到OutOfMemoryError似乎有9个实例(即使我们有足够的内存):

    > jmap -histo 12103 | grep OutOfMemoryError
     71:             9            288  java.lang.OutOfMemoryError
    170:             1             32  [Ljava.lang.OutOfMemoryError;
    

    然后我们可以生成堆转储:

    > jmap -dump:format=b,file=heap.hprof 12315
    

    并使用Eclipse Memory Analyzer打开它,其中OQL查询显示JVM实际上似乎为所有可能的消息预分配 OutOfMemoryErrors

    enter image description here

    Java 8 Hotspot JVM的代码实际上预先分配了这些can be found here,看起来像这样(省略了一些部分):

    ...
    // Setup preallocated OutOfMemoryError errors
    k = SystemDictionary::resolve_or_fail(vmSymbols::java_lang_OutOfMemoryError(), true, CHECK_false);
    k_h = instanceKlassHandle(THREAD, k);
    Universe::_out_of_memory_error_java_heap = k_h->allocate_instance(CHECK_false);
    Universe::_out_of_memory_error_metaspace = k_h->allocate_instance(CHECK_false);
    Universe::_out_of_memory_error_class_metaspace = k_h->allocate_instance(CHECK_false);
    Universe::_out_of_memory_error_array_size = k_h->allocate_instance(CHECK_false);
    Universe::_out_of_memory_error_gc_overhead_limit =
      k_h->allocate_instance(CHECK_false);
    
    ...
    
    if (!DumpSharedSpaces) {
      // These are the only Java fields that are currently set during shared space dumping.
      // We prefer to not handle this generally, so we always reinitialize these detail messages.
      Handle msg = java_lang_String::create_from_str("Java heap space", CHECK_false);
      java_lang_Throwable::set_message(Universe::_out_of_memory_error_java_heap, msg());
    
      msg = java_lang_String::create_from_str("Metaspace", CHECK_false);
      java_lang_Throwable::set_message(Universe::_out_of_memory_error_metaspace, msg());
      msg = java_lang_String::create_from_str("Compressed class space", CHECK_false);
      java_lang_Throwable::set_message(Universe::_out_of_memory_error_class_metaspace, msg());
    
      msg = java_lang_String::create_from_str("Requested array size exceeds VM limit", CHECK_false);
      java_lang_Throwable::set_message(Universe::_out_of_memory_error_array_size, msg());
    
      msg = java_lang_String::create_from_str("GC overhead limit exceeded", CHECK_false);
      java_lang_Throwable::set_message(Universe::_out_of_memory_error_gc_overhead_limit, msg());
    
      msg = java_lang_String::create_from_str("/ by zero", CHECK_false);
      java_lang_Throwable::set_message(Universe::_arithmetic_exception_instance, msg());
    
      // Setup the array of errors that have preallocated backtrace
      k = Universe::_out_of_memory_error_java_heap->klass();
      assert(k->name() == vmSymbols::java_lang_OutOfMemoryError(), "should be out of memory error");
      k_h = instanceKlassHandle(THREAD, k);
    
      int len = (StackTraceInThrowable) ? (int)PreallocatedOutOfMemoryErrorCount : 0;
      Universe::_preallocated_out_of_memory_error_array = oopFactory::new_objArray(k_h(), len, CHECK_false);
      for (int i=0; i<len; i++) {
        oop err = k_h->allocate_instance(CHECK_false);
        Handle err_h = Handle(THREAD, err);
        java_lang_Throwable::allocate_backtrace(err_h, CHECK_false);
        Universe::preallocated_out_of_memory_errors()->obj_at_put(i, err_h());
      }
      Universe::_preallocated_out_of_memory_error_avail_count = (jint)len;
    }
    ...
    

    this code表明JVM将首先尝试使用其中一个预先分配的错误和空间进行堆栈跟踪,然后回退到没有堆栈跟踪的错误:

    oop Universe::gen_out_of_memory_error(oop default_err) {
      // generate an out of memory error:
      // - if there is a preallocated error with backtrace available then return it wth
      //   a filled in stack trace.
      // - if there are no preallocated errors with backtrace available then return
      //   an error without backtrace.
      int next;
      if (_preallocated_out_of_memory_error_avail_count > 0) {
        next = (int)Atomic::add(-1, &_preallocated_out_of_memory_error_avail_count);
        assert(next < (int)PreallocatedOutOfMemoryErrorCount, "avail count is corrupt");
      } else {
        next = -1;
      }
      if (next < 0) {
        // all preallocated errors have been used.
        // return default
        return default_err;
      } else {
        // get the error object at the slot and set set it to NULL so that the
        // array isn't keeping it alive anymore.
        oop exc = preallocated_out_of_memory_errors()->obj_at(next);
        assert(exc != NULL, "slot has been used already");
        preallocated_out_of_memory_errors()->obj_at_put(next, NULL);
    
        // use the message from the default error
        oop msg = java_lang_Throwable::message(default_err);
        assert(msg != NULL, "no message");
        java_lang_Throwable::set_message(exc, msg);
    
        // populate the stack trace and return it.
        java_lang_Throwable::fill_in_stack_trace_of_preallocated_backtrace(exc);
        return exc;
      }
    }
    

相关问题