java - que - ¿Qué sucede cuando no hay suficiente memoria para lanzar un OutOfMemoryError?
java heap space solucion (11)
Soy consciente de que cada objeto requiere memoria de pila y cada referencia / primitiva en la pila requiere memoria de pila.
Cuando intento crear un objeto en el montón y no hay suficiente memoria para hacerlo, la JVM crea un java.lang.OutOfMemoryError en el montón y me lo lanza.
Entonces, implícitamente, esto significa que hay algo de memoria reservada por la JVM en el inicio.
¿Qué sucede cuando se usa esta memoria reservada (definitivamente se usará, lea la discusión a continuación) y JVM no tiene suficiente memoria en el montón para crear una instancia de java.lang.OutOfMemoryError ?
¿Simplemente se cuelga? ¿O me lanzaría un null
ya que no hay memoria para una new
instancia de 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
}
==
¿Por qué no pudo la JVM reservar suficiente memoria?
No importa cuánta memoria se reserve, todavía es posible que esa memoria se use si la JVM no tiene una forma de "reclamar" esa memoria:
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
}
}
}
}
}
}
O más concisamente:
private void OnOOM(java.lang.OutOfMemoryError e) {
try {
e.printStackTrace();
} catch (java.lang.OutOfMemoryError e2) {
OnOOM(e2);
}
}
De la especificación JVM, Capítulo 3.5.2:
Si las pilas de máquinas virtuales de Java se pueden expandir dinámicamente y se intenta una expansión, pero no se puede disponer de suficiente memoria para efectuar la expansión, o si se puede disponer de una memoria insuficiente para crear la pila de máquinas virtuales de Java inicial para un nuevo hilo, el Java virtual la máquina lanza un
OutOfMemoryError
.
Cada máquina virtual Java tiene que garantizar que lanzará un OutOfMemoryError
. Eso implica que tiene que ser capaz de crear una instancia de OutOfMemoryError
(o haberse creado con antelación) incluso si no queda espacio en el montón.
Aunque no tiene que garantizar, hay suficiente memoria para capturarlo e imprimir un buen seguimiento de pila ...
Adición
Agregó algo de código para mostrar que la JVM podría quedarse sin espacio de OutOfMemoryError
dinámico si tuviera que lanzar más de un OutOfMemoryError
. Pero tal implementación violaría el requisito de arriba.
No hay ningún requisito de que las instancias lanzadas de OutOfMemoryError
sean únicas o creadas a pedido. Una JVM podría preparar exactamente una instancia de OutOfMemoryError
durante el inicio y lanzar esto cada vez que se quede sin espacio de OutOfMemoryError
una vez, en un entorno normal. En otras palabras: la instancia de OutOfMemoryError
que vemos podría ser un singleton.
Estoy bastante seguro de que la JVM se asegurará de que tenga al menos suficiente memoria para lanzar una excepción antes de que se quede sin memoria.
Interesante pregunta :-). Mientras que los otros han dado buenas explicaciones de los aspectos teóricos, decidí probarlo. Esto está en Oracle JDK 1.6.0_26, Windows 7 64 bit.
Configuración de prueba
Escribí un programa simple para agotar la memoria (ver más abajo).
El programa simplemente crea una java.util.List
estática, y continúa metiendo nuevas cadenas en ella, hasta que se lanza OOM. Luego lo atrapa y continúa rellenando en un bucle sin fin (mala JVM ...).
Resultado de la prueba
Como se puede ver en la salida, las primeras cuatro veces que se lanza OOME, viene con un seguimiento de pila. Después de eso, los OOME subsiguientes solo imprimen java.lang.OutOfMemoryError: Java heap space
printStackTrace()
java.lang.OutOfMemoryError: Java heap space
si se invoca printStackTrace()
.
Así que aparentemente, la JVM hace un esfuerzo para imprimir un seguimiento de la pila si puede, pero si la memoria es muy limitada, simplemente omite el rastreo, tal como lo sugieren las otras respuestas.
También es interesante el código hash del OOME. Tenga en cuenta que los primeros OOME tienen hashes diferentes. Una vez que la JVM comienza a omitir los seguimientos de pila, el hash es siempre el mismo. Esto sugiere que la JVM usará instancias OOME nuevas (¿preasignadas?) El mayor tiempo posible, pero si se produce un empuje, simplemente reutilizará la misma instancia en lugar de no tener nada que lanzar.
Salida
Nota: truncé algunos seguimientos de pila para facilitar la lectura ("[...]").
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...
[...]
El programa
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);
}
}
}
}
La última vez que estuve trabajando en Java y utilizando un depurador, el inspector de almacenamiento mostró que la JVM asignó una instancia de OutOfMemoryError en el inicio. En otras palabras, asigna el objeto antes de que su programa tenga la oportunidad de comenzar a consumir, y mucho menos se quede sin memoria.
La JVM nunca se queda realmente sin memoria. Hace el cálculo de memoria de la pila de pila de antemano.
La Estructura de la JVM, Capítulo 3 , sección 3.5.2 establece:
- Si las pilas de máquinas virtuales de Java se pueden expandir dinámicamente y se intenta una expansión, pero no se puede disponer de suficiente memoria para efectuar la expansión, o si se puede disponer de una memoria insuficiente para crear la pila de máquinas virtuales de Java inicial para un nuevo hilo, el Java virtual la máquina lanza un
OutOfMemoryError
.
Para Heap , Sección 3.5.3.
- Si un cálculo requiere más almacenamiento que el que puede hacer disponible el sistema de administración de almacenamiento automático, la máquina virtual de Java lanza un
OutOfMemoryError
.
Por lo tanto, realiza un cálculo por adelantado antes de realizar la asignación del objeto.
Lo que sucede es que la JVM intenta asignar memoria para un objeto en la memoria llamada región de generación permanente (o PermSpace). Si la asignación falla (incluso después de que la JVM invoca al recolector de basura para probar y asignar espacio libre), lanza un OutOfMemoryError
. Incluso las excepciones requieren un espacio de memoria, por lo que el error se producirá de forma indefinida.
Otras lecturas. ? Además, OutOfMemoryError
puede ocurrir en diferentes estructuras de JVM.
La mayoría de los entornos de tiempo de ejecución se asignarán previamente al inicio, o de lo contrario se reservará, suficiente memoria para hacer frente a situaciones de falta de memoria. Me imagino que la mayoría de las implementaciones de JVM sanas harían esto.
Las excepciones que indican un intento de violar los límites de un entorno de memoria administrada son manejadas por el tiempo de ejecución de dicho entorno, en este caso la JVM. La JVM es su propio proceso, que ejecuta el IL de su aplicación. En caso de que un programa intente realizar una llamada que extienda la pila de llamadas más allá de los límites, o asigne más memoria de la que puede reservar la JVM, el propio tiempo de ejecución inyectará una excepción, lo que provocará que la pila de llamadas se desenvuelva. Independientemente de la cantidad de memoria que su programa necesita actualmente, o la profundidad de su pila de llamadas, la JVM habrá asignado suficiente memoria dentro de sus propios límites de proceso para crear dicha excepción e inyectarla en su código.
Las respuestas que dicen que la JVM pre-asignará OutOfMemoryErrors
son correctas.
Además de probar esto provocando una situación de falta de memoria, solo podemos verificar el montón de cualquier JVM (utilicé un pequeño programa que solo duerme, ejecutándolo con Oracle Hotspot JVM de Java 8 actualización 31).
Al usar jmap
, vemos que parece haber 9 instancias de OutOfMemoryError (aunque tenemos mucha memoria):
> jmap -histo 12103 | grep OutOfMemoryError 71: 9 288 java.lang.OutOfMemoryError 170: 1 32 [Ljava.lang.OutOfMemoryError;
Entonces podemos generar un volcado de pila:
> jmap -dump:format=b,file=heap.hprof 12315
y ábralo con Eclipse Memory Analyzer , donde una consulta OQL muestra que la JVM en realidad parece asignar OutOfMemoryErrors
para todos los mensajes posibles:
El código para la JVM de Hotspot de Java 8 que en realidad preasigna estos se puede encontrar aquí , y se ve así (con algunas partes omitidas):
...
// 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;
}
...
y este código muestra que la JVM primero intentará usar uno de los errores preasignados con espacio para un seguimiento de pila, y luego volverá a uno sin un seguimiento de pila:
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;
}
}
Para aclarar aún más la respuesta de @Graham Borland, funcionalmente, la JVM hace esto al inicio:
private static final OutOfMemoryError OOME = new OutOfMemoryError();
Más adelante, la JVM ejecuta uno de los siguientes códigos de byte de Java: ''nuevo'', ''anewarray'' o ''multianewarray''. Esta instrucción hace que la JVM realice una serie de pasos en una condición de memoria insuficiente:
- Invoque una función nativa, digamos
allocate()
.allocate()
intenta asignar memoria para algunas instancias nuevas de una clase o matriz en particular. - La solicitud de asignación falla, por lo que la JVM invoca otra función nativa, por ejemplo,
doGC()
, que intenta realizar una recolección de basura. - Cuando esa función regresa,
allocate()
intenta asignar memoria para la instancia una vez más. - Si eso falla (*), entonces la JVM, en allocate (), simplemente
throw OOME;
unthrow OOME;
, refiriéndose a la OOME que se instancia en el inicio. Tenga en cuenta que no tuvo que asignar ese OOME, simplemente se refiere a él.
Obviamente, estos no son pasos literales; variarán de JVM a JVM en la implementación, pero esta es la idea de alto nivel.
(*) Una cantidad significativa de trabajo ocurre aquí antes de fallar. La JVM intentará borrar los objetos SoftReference, intentará la asignación directamente en la generación titular cuando se usa un recopilador generacional, y posiblemente otras cosas, como la finalización.
Parece estar confundiendo la memoria virtual reservada por la JVM en la que la JVM ejecuta programas Java con la memoria nativa del SO host en la que se ejecuta la JVM como un proceso nativo. La JVM en su máquina se ejecuta en la memoria administrada por el sistema operativo, no en la memoria que la JVM ha reservado para ejecutar programas Java.
Otras lecturas:
- http://java.sys-con.com/node/1229281
- http://publib.boulder.ibm.com/infocenter/javasdk/tools/index.jsp?topic=%2Fcom.ibm.java.doc.igaa%2F_1vg000121410cbe-1195c23a635-7ffa_1001.html
Y como nota final, intentar capturar un java.lang.Error (y sus clases descendientes) para imprimir un stacktrace puede no darte información útil. Quieres un volcado de pila en su lugar.
Graham Borland parece tener razón : al menos mi JVM aparentemente reutiliza OutOfMemoryErrors. Para probar esto, escribí un programa de prueba simple:
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);
}
}
Ejecutándolo produce esta salida:
$ javac OOMTest.java && java -Xmx10m OOMTest
Got the same OutOfMemoryError twice: java.lang.OutOfMemoryError: Java heap space
Por cierto, la JVM que estoy ejecutando (en Ubuntu 10.04) es la siguiente:
$ 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)
Edición: traté de ver qué pasaría si forzaba a la JVM a quedarse completamente sin memoria utilizando el siguiente programa:
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);
}
}
Como resultado, parece que se repite para siempre. Sin embargo, curiosamente, tratar de terminar el programa con Ctrl + C no funciona, pero solo da el siguiente mensaje:
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