当内存不足抛出OutOfMemoryError时会发生什么?

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

当我尝试在堆上创建一个对象,但内存不足时,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
}

= =

为什么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);
}
}
33063 次浏览

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

我非常确定,JVM将绝对确保它至少有足够的内存在内存耗尽之前抛出异常。

JVM从来不会真正耗尽内存。它提前对堆堆栈进行内存计算。

JVM结构,第3章,第3.5.2节声明:

  • 如果Java虚拟机栈可以动态扩展,并且尝试扩展但可用内存不足 以影响扩展,或如果内存不足,可以作出 可用来创建一个新的初始Java虚拟机堆栈 线程时,Java虚拟机抛出OutOfMemoryError.
  • .线程

对于,第3.5.3节。

  • 如果一个计算需要的堆超过了自动存储管理系统所能提供的堆,Java虚拟机
  • .抛出OutOfMemoryError

在分配对象之前,它会提前进行计算。


JVM尝试为内存中称为永久生成区域(或PermSpace)的对象分配内存。如果分配失败(即使在JVM调用垃圾收集器尝试&分配空闲空间),则抛出OutOfMemoryError。即使异常也需要内存空间,因此错误将被无限期地抛出。

进一步阅读。吗?此外,OutOfMemoryError可以出现在不同的JVM结构。

摘自JVM规范第3.5.2章:

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

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

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

除了

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

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

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-Bit Server VM warning: Exception java.lang.OutOfMemoryError occurred dispatching signal SIGINT to handler- the VM may need to be forcibly terminated

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

您似乎混淆了JVM保留的虚拟内存(JVM在其中运行Java程序)和主机操作系统的本机内存(JVM在其中作为本机进程运行)。您机器上的JVM运行在由操作系统管理的内存中,而不是JVM预留用于运行Java程序的内存中。

进一步阅读:

最后需要注意的是,尝试 java.lang.Error(及其子类)以输出stacktrace可能不会提供任何有用的信息。相反,您需要一个堆转储。

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

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

测试设置

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

该程序只是创建一个静态java.util.List,并不断向其填充新的字符串,直到抛出OOM。然后它捕获它,并继续在一个无休止的循环中填充(可怜的JVM…)

测试结果

从输出中可以看到,抛出OOME的前四次都带有堆栈跟踪。在此之后,如果调用了printStackTrace(),后续的oome只打印java.lang.OutOfMemoryError: Java heap space

因此,JVM显然会尽可能地输出堆栈跟踪,但如果内存非常紧张,它就会像其他答案所建议的那样忽略跟踪。

同样有趣的是OOME的哈希代码。注意,前几个OOME都有不同的哈希值。一旦JVM开始省略堆栈跟踪,散列总是相同的。这表明JVM将使用fresh(预分配?)尽可能长地使用OOME实例,但如果出现紧急情况,它将重用相同的实例,而不是没有任何可抛出的实例。

输出

注意:我截断了一些堆栈跟踪,以使输出更容易阅读(“[…]”)。

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...
[...]

这个项目

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);
}
}


}
}

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

private static final OutOfMemoryError OOME = new OutOfMemoryError();

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

  1. 调用一个本地函数,比如allocate()allocate()尝试为某些特定类或数组的新实例分配内存。
  2. 分配请求失败,因此JVM调用另一个本机函数,例如doGC(),该函数试图进行垃圾收集。
  3. 当该函数返回时,allocate()再次尝试为实例分配内存。
  4. 如果失败(*),那么JVM在allocate()中简单地执行throw OOME;,引用它在启动时实例化的OOME。注意,它不必分配那个OOME,它只是引用它。

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

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

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:

enter image description here

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