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

当我试图在堆上创建一个对象,而内存不足时,JVM会在堆上创建一个java.lang.OutOfMemoryError,并将它抛出给我。

因此,这意味着JVM在启动时保留了一些内存。

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

它只是挂着吗?或者他会给我一个空,因为没有内存新的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
}

==

为什么JVM不能预留足够的内存?

无论保留了多少内存,如果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);
    }
}

当前回答

摘自JVM规范第3.5.2章:

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

每个Java虚拟机都必须保证抛出OutOfMemoryError错误。这意味着,即使没有堆空间,它也必须能够创建OutOfMemoryError的实例(或预先创建一个)。

虽然它不需要保证有足够的内存来捕获它并打印一个漂亮的堆栈跟踪…

除了

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

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

其他回答

为了进一步澄清@Graham Borland的回答,从功能上讲,JVM在启动时这样做:

private static final OutOfMemoryError OOME = new OutOfMemoryError();

稍后,JVM执行以下Java字节码之一:'new', 'anewarray',或'multianewarray'。这条指令导致JVM在内存不足的情况下执行一些步骤:

Invoke a native function, say allocate(). allocate() attempts to allocate memory for some a new instance of a particular class or array. That allocation request fails, so the JVM invokes another native function, say doGC(), which attempts to do garbage collection. When that function returns, allocate() tries to allocate memory for the instance once again. If that fails(*), then the JVM, within allocate(), simply does a throw OOME;, referring to the OOME that it instantiated at startup. Note that it did not have to allocate that OOME, it just refers to it.

显然,这些都不是字面上的步骤;它们在实现中会因JVM的不同而不同,但这是高级的想法。

在失败之前,这里发生了大量的工作。JVM将尝试清除SoftReference对象,在使用分代收集器时尝试直接分配到年老代,以及可能的其他事情,比如终结。

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

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

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

> 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内存分析器打开它,其中OQL查询显示JVM实际上似乎为所有可能的消息预分配OutOfMemoryErrors:

Java 8 Hotspot JVM的代码可以在这里找到,看起来像这样(有些部分被省略了):

...
// 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;
}
...

这段代码表明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;
  }
}

Graham Borland似乎是对的:至少我的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)

编辑:我试着看看如果我使用以下程序强制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位服务器虚拟机警告:Java .lang. outofmemoryerror发生异常,向处理器发送SIGINT信号-虚拟机可能需要强制终止

摘自JVM规范第3.5.2章:

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

每个Java虚拟机都必须保证抛出OutOfMemoryError错误。这意味着,即使没有堆空间,它也必须能够创建OutOfMemoryError的实例(或预先创建一个)。

虽然它不需要保证有足够的内存来捕获它并打印一个漂亮的堆栈跟踪…

除了

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

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