Capturando java.lang.OutOfMemoryError?
try-catch out-of-memory (14)
- Depende de cómo defines "bueno". Hacemos eso en nuestra aplicación web con errores y funciona la mayor parte del tiempo (afortunadamente, ahora
OutOfMemory
no ocurre debido a una solución no relacionada). Sin embargo, incluso si lo detecta, aún podría haber roto algún código importante: si tiene varios subprocesos, la asignación de memoria puede fallar en cualquiera de ellos. Entonces, dependiendo de su aplicación, todavía hay un 10--90% de probabilidad de que se rompa irreversiblemente. - Por lo que yo entiendo, la pila pesada que se desenrolla en el camino invalidará tantas referencias y, por lo tanto, liberará tanta memoria que no debería importarle eso.
EDITAR: Te sugiero que lo pruebes. Digamos, escribe un programa que recursivamente llama a una función que asigna progresivamente más memoria. OutOfMemoryError
y vea si puede continuar significativamente desde ese punto. De acuerdo con mi experiencia, podrás hacerlo, aunque en mi caso sucedió en el servidor de WebLogic, por lo que podría haber algo de magia negra involucrada.
Documentation para java.lang.Error
dice:
Un error es una subclase de Throwable que indica problemas graves que una aplicación razonable no debería intentar atrapar
Pero como java.lang.Error
es una subclase de java.lang.Throwable
, puedo atrapar este tipo de Throwable.
Entiendo por qué no es buena idea atrapar este tipo de excepción. Por lo que yo entiendo, si decidimos atraparlo, el controlador de captura no debe asignar memoria por sí mismo. De OutOfMemoryError
contrario, OutOfMemoryError
se OutOfMemoryError
nuevamente.
Entonces, mi pregunta es:
- ¿Hay algún escenario de palabras real cuando capturar
java.lang.OutOfMemoryError
puede ser una buena idea? - Si decidimos atrapar
java.lang.OutOfMemoryError
, ¿cómo podemos estar seguros de que el controlador catch no asigna memoria por sí mismo (ninguna herramienta o mejores prácticas)?
Definitivamente hay escenarios en los que capturar un OOME tiene sentido. IDEA los detecta y abre un cuadro de diálogo que le permite cambiar la configuración de la memoria de inicio (y luego sale cuando haya terminado). Un servidor de aplicaciones podría atraparlos e informarlos. La clave para hacer esto es hacerlo a un alto nivel en el despacho para que tenga una posibilidad razonable de liberar muchos recursos en el punto en el que está atrapando la excepción.
Además del escenario IDEA anterior, en general, la captura debería ser Throwable, no solo OOM específicamente, y debería hacerse en un contexto donde al menos el hilo terminará en breve.
Por supuesto, la mayoría de las veces la memoria está muerta de hambre y la situación no es recuperable, pero hay formas en que tiene sentido.
En general, es una mala idea tratar de atrapar y recuperar un OOM.
Un OOME también podría haber sido lanzado en otros hilos, incluidos los hilos que su aplicación ni siquiera conoce. Cualquiera de estos hilos ahora estará muerto, y todo lo que estaba esperando en una notificación podría estar atascado para siempre. En resumen, tu aplicación podría estar terminalmente rota.
Incluso si se recupera con éxito, su JVM puede seguir sufriendo de inanición y su aplicación tendrá un rendimiento abismal como resultado.
Lo mejor que puedes hacer con un OOME es dejar que la JVM muera.
(Esto supone que la JVM sí muere. Por ejemplo, los OOM en un subproceso de servlet Tomcat no matan a la JVM, y esto lleva a Tomcat a entrar en un estado catatónico donde no responderá a ninguna solicitud ... ni siquiera solicitudes a reiniciar.)
EDITAR
No estoy diciendo que sea una mala idea atrapar OOM en absoluto. Los problemas surgen cuando luego intentas recuperarte del OOME, ya sea deliberadamente o por descuido. Siempre que capture un OOM (directamente, o como un subtipo de Error o Throwable), debe volver a lanzarlo, o disponer que la aplicación / JVM salga.
Aparte: Esto sugiere que para una robustez máxima frente a los OOM una aplicación debería usar Thread.setDefaultUncaughtExceptionHandler() para establecer un controlador que hará que la aplicación salga en el caso de un OOME, sin importar en qué subproceso se lanza el OOME. Me interesarían las opiniones sobre esto ...
El único otro escenario es cuando sabes con certeza que el OOM no ha producido ningún daño colateral; es decir, usted sabe:
- qué específicamente causó el OOME,
- qué estaba haciendo la aplicación en ese momento, y que está bien descartar ese cálculo, y
- que un OOME (aproximadamente) simultáneo no puede haber ocurrido en otro hilo.
Hay aplicaciones en las que es posible saber estas cosas, pero para la mayoría de las aplicaciones no puede estar seguro de que la continuación después de un OOME sea segura. Incluso si empíricamente "funciona" cuando lo intentas.
(El problema es que se requiere una prueba formal para demostrar que las consecuencias de las OOMEs "anticipadas" son seguras, y que las OOME "imprevistas" no pueden ocurrir dentro del control de una OOME try / catch).
Estoy de acuerdo y en desacuerdo con la mayoría de las respuestas aquí.
Hay una serie de escenarios en los que es posible que desee capturar un OutOfMemoryError
y, según mi experiencia (en Windows y Solaris JVM), solo muy infrecuentemente OutOfMemoryError
es el OutOfMemoryError
de una JVM.
Solo hay una buena razón para atrapar un OutOfMemoryError
y es cerrar con elegancia, liberando recursos limpiamente y registrando el motivo de la falla lo mejor que pueda (si es posible hacerlo).
En general, OutOfMemoryError
produce debido a una asignación de memoria de bloque que no se puede satisfacer con los recursos restantes del montón.
Cuando se Error
el Error
el montón contiene la misma cantidad de objetos asignados que antes de la asignación incorrecta y ahora es el momento de dejar caer las referencias a los objetos en tiempo de ejecución para liberar aún más memoria que puede ser necesaria para la limpieza. En estos casos, incluso es posible continuar, pero definitivamente sería una mala idea, ya que nunca se puede estar 100% seguro de que la JVM se encuentra en un estado reparable.
Demostración de que OutOfMemoryError
no significa que la JVM está fuera de memoria en el bloque catch.
private static final int MEGABYTE = (1024*1024);
public static void runOutOfMemory() {
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
for (int i=1; i <= 100; i++) {
try {
byte[] bytes = new byte[MEGABYTE*500];
} catch (Exception e) {
e.printStackTrace();
} catch (OutOfMemoryError e) {
MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
long maxMemory = heapUsage.getMax() / MEGABYTE;
long usedMemory = heapUsage.getUsed() / MEGABYTE;
System.out.println(i+ " : Memory Use :" + usedMemory + "M/" + maxMemory + "M");
}
}
}
Salida de este código:
1 : Memory Use :0M/247M
..
..
..
98 : Memory Use :0M/247M
99 : Memory Use :0M/247M
100 : Memory Use :0M/247M
Si ejecuta algo crítico, generalmente capturo el Error
, lo conecto a syserr, luego lo registro usando mi marco de trabajo de registro de elección, luego procedo a liberar recursos y cerrar de manera limpia. ¿Qué es lo peor que puede pasar? La JVM se está muriendo (o ya está muerta) de todos modos y al detectar el Error
hay al menos una posibilidad de limpieza.
La advertencia es que debe enfocarse en la captura de este tipo de errores solo en lugares donde la limpieza es posible. No bloquee la catch(Throwable t) {}
todas partes o tonterías como esa.
La única razón por la que puedo pensar por qué la captura de errores OOM podría ser que tiene algunas estructuras de datos masivas que ya no está utilizando, y puede establecer nulo y liberar algo de memoria. Pero (1) eso significa que estás desperdiciando memoria, y deberías arreglar tu código en lugar de cojear después de un OOME, y (2) incluso si lo capturaste, ¿qué harías? OOM puede suceder en cualquier momento, potencialmente dejando todo a medio hacer.
Me encontré con esta pregunta porque me preguntaba si es una buena idea atrapar OutOfMemoryError en mi caso. Estoy respondiendo aquí parcialmente para mostrar otro ejemplo cuando detectar este error puede tener sentido para alguien (es decir, para mí) y parcialmente para descubrir si es una buena idea en mi caso (ya que soy un desarrollador súper junior nunca podré estar demasiado seguro sobre cualquier línea de código que escriba).
De todos modos, estoy trabajando en una aplicación de Android que se puede ejecutar en diferentes dispositivos con diferentes tamaños de memoria. La parte peligrosa es descodificar un mapa de bits de un archivo y distribuirlo en una instancia de ImageView. No quiero restringir los dispositivos más poderosos en términos del tamaño del mapa de bits decodificado, ni puedo estar seguro de que la aplicación no se ejecute en algún dispositivo antiguo que nunca he encontrado con muy poca memoria. Por lo tanto, hago esto:
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
bitmapOptions.inSampleSize = 1;
boolean imageSet = false;
while (!imageSet) {
try {
image = BitmapFactory.decodeFile(filePath, bitmapOptions);
imageView.setImageBitmap(image);
imageSet = true;
}
catch (OutOfMemoryError e) {
bitmapOptions.inSampleSize *= 2;
}
}
De esta manera, logro proporcionar dispositivos cada vez más potentes de acuerdo con sus necesidades o expectativas, o mejor dicho, con sus usuarios.
OOME puede ser capturado, pero generalmente será inútil, dependiendo de si la JVM puede recoger algunos objetos cuando se alcanza la captura, y la cantidad de memoria de almacenamiento que queda para ese momento.
Ejemplo: en mi JVM, este programa se ejecuta hasta su finalización:
import java.util.LinkedList;
import java.util.List;
public class OOMErrorTest {
public static void main(String[] args) {
List<Long> ll = new LinkedList<Long>();
try {
long l = 0;
while(true){
ll.add(new Long(l++));
}
} catch(OutOfMemoryError oome){
System.out.println("Error catched!!");
}
System.out.println("Test finished");
}
}
Sin embargo, solo agregar una sola línea en la captura le mostrará de lo que estoy hablando:
import java.util.LinkedList;
import java.util.List;
public class OOMErrorTest {
public static void main(String[] args) {
List<Long> ll = new LinkedList<Long>();
try {
long l = 0;
while(true){
ll.add(new Long(l++));
}
} catch(OutOfMemoryError oome){
System.out.println("Error catched!!");
System.out.println("size:" +ll.size());
}
System.out.println("Test finished");
}
}
El primer programa funciona bien porque cuando se alcanza la captura, la JVM detecta que la lista ya no se usará más (esta detección también puede ser una optimización realizada en tiempo de compilación). Entonces, cuando llegamos a la declaración de impresión, la memoria del montón se ha liberado casi por completo, por lo que ahora tenemos un amplio margen de maniobra para continuar. Este es el mejor caso.
Sin embargo, si el código está organizado, por ejemplo, si la lista ll
se utiliza después de que el OOME ha sido atrapado, la JVM no podrá recopilarlo. Esto sucede en el segundo fragmento. El OOME, desencadenado por una nueva creación Long, queda atrapado, pero pronto estamos creando un nuevo Object (una Cadena en System.out,println
), y el montón está casi lleno, por lo que se lanza un nuevo OOME. Este es el peor de los casos: intentamos crear un nuevo objeto, fallamos, atrapamos el OOME, sí, pero ahora la primera instrucción que requiere memoria de pila nueva (por ejemplo, crear un nuevo objeto) lanzará un nuevo OOME. Piénselo, ¿qué más podemos hacer en este momento con tan poco recuerdo? Probablemente acaba de salir. De ahí lo inútil.
Entre los motivos por los que la JVM no recopila recursos, uno da mucho miedo: un recurso compartido con otros hilos también hace uso de él. Cualquier persona con un cerebro puede ver cuán peligroso puede ser atrapar a OOME si se lo inserta en alguna aplicación no experimental de ningún tipo.
Estoy usando un Windows x86 32bits JVM (JRE6). La memoria predeterminada para cada aplicación Java es de 64 MB.
Para la pregunta 2 ya veo la solución que sugeriría, por BalusC.
- ¿Hay algún escenario de palabras real cuando capturar java.lang.OutOfMemoryError puede ser una buena idea?
Creo que acabo de encontrar un buen ejemplo. Cuando una aplicación awt está enviando mensajes, OutOfMemoryError no detectado se muestra en stderr y se detiene el procesamiento del mensaje actual. ¡Pero la aplicación sigue ejecutándose! El usuario aún puede emitir otros comandos sin darse cuenta de los graves problemas que ocurren detrás de la escena. Especialmente cuando no puede o no observa el error estándar. Así que capturar una excepción y proporcionar (o al menos sugerir) reinicio de la aplicación es algo deseado.
Puede capturar cualquier cosa en Throwable, en general, solo debe atrapar subclases de Exception, excepto RuntimeException (aunque una gran parte de los desarrolladores también detecta RuntimeException ... pero esa nunca fue la intención de los diseñadores de idiomas).
Si capturaras OutOfMemoryError, ¿qué demonios harías? La VM está sin memoria, básicamente, todo lo que puedes hacer es salir. Probablemente ni siquiera puede abrir un cuadro de diálogo para decirles que se ha quedado sin memoria, ya que eso llevaría la memoria :-)
La VM arroja un OutOfMemoryError cuando está realmente fuera de memoria (de hecho, todos los errores deberían indicar situaciones irrecuperables) y realmente no debería haber nada que pueda hacer para solucionarlo.
Lo que debe hacer es averiguar por qué se está quedando sin memoria (use un generador de perfiles, como el de NetBeans) y asegúrese de no tener pérdidas de memoria. Si no tiene pérdidas de memoria, aumente la memoria que asigna a la VM.
Puedes recuperarte de esto:
package com..q2679330;
public class Test {
public static void main(String... args) {
int size = Integer.MAX_VALUE;
int factor = 10;
while (true) {
try {
System.out.println("Trying to allocate " + size + " bytes");
byte[] bytes = new byte[size];
System.out.println("Succeed!");
break;
} catch (OutOfMemoryError e) {
System.out.println("OOME .. Trying again with 10x less");
size /= factor;
}
}
}
}
Pero tiene sentido? ¿Qué más te gustaría hacer? ¿Por qué asignaría inicialmente tanta memoria? ¿Menos memoria también está bien? ¿Por qué ya no lo usas? O si eso no es posible, ¿por qué no simplemente darle a la JVM más memoria desde el principio?
De vuelta a tus preguntas:
1: ¿hay algún escenario de palabras real cuando capturar java.lang.OutOfMemoryError puede ser una buena idea?
Ninguno viene a la mente.
2: si capturamos java.lang.OutOfMemoryError, ¿cómo podemos estar seguros de que el controlador catch no asigna memoria por sí mismo (ninguna herramienta o mejores prácticas)?
Depende de lo que ha causado el OOME. Si se declara fuera del bloque try
y ocurre paso a paso, entonces tus posibilidades son pocas. Es posible que desee reservar algo de espacio de memoria de antemano:
private static byte[] reserve = new byte[1024 * 1024]; // Reserves 1MB.
y luego configurarlo a cero durante OOME:
} catch (OutOfMemoryException e) {
reserve = new byte[0];
// Ha! 1MB free!
}
Por supuesto, esto hace que todo no tenga sentido;) Simplemente déle a JVM suficiente memoria como lo requiera su aplicación. Ejecute un generador de perfiles si es necesario.
Sí, hay escenarios del mundo real. Aquí está el mío: necesito procesar conjuntos de datos de muchos elementos en un clúster con memoria limitada por nodo. Una instancia de JVM determinada pasa por varios elementos, uno tras otro, pero algunos de los elementos son demasiado grandes para procesarlos en el clúster: puedo detectar OutOfMemoryError
y tomar nota de qué elementos son demasiado grandes. Más tarde, puedo volver a ejecutar solo los elementos grandes en una computadora con más RAM.
(Debido a que es una asignación de varios gigabytes de una matriz que falla, la JVM sigue estando bien después de detectar el error y hay suficiente memoria para procesar los otros elementos).
Sí, la verdadera pregunta es "¿qué vas a hacer en el manejador de excepciones?" Para casi cualquier cosa útil, asignará más memoria. Si desea realizar algún trabajo de diagnóstico cuando se produce un OutOfMemoryError, puede utilizar el -XX:OnOutOfMemoryError=<cmd>
suministrado por la máquina virtual de HotSpot. Ejecutará tus comandos cuando se produzca un OutOfMemoryError y podrás hacer algo útil fuera del montón de Java. Realmente desea evitar que la aplicación se quede sin memoria en primer lugar, por lo que averiguar el motivo por el que sucede es el primer paso. Luego puede aumentar el tamaño de almacenamiento dinámico de MaxPermSize según corresponda. Aquí hay algunos otros enlaces útiles de HotSpot:
-XX:+PrintCommandLineFlags
-XX:+PrintConcurrentLocks
-XX:+PrintClassHistogram
Vea la lista completa here
Solo tengo un escenario donde capturar un OutOfMemoryError parece tener sentido y parece funcionar.
Escenario: en una aplicación para Android, quiero mostrar mapas de bits múltiples con la mayor resolución posible, y quiero poder hacer zoom en ellos con fluidez.
Debido al zoom fluido, quiero tener los mapas de bits en la memoria. Sin embargo, Android tiene limitaciones en la memoria que dependen del dispositivo y que son difíciles de controlar.
En esta situación, puede haber OutOfMemoryError al leer el mapa de bits. Aquí, ayuda si lo atrapo y luego continúo con una resolución más baja.
Tengo una aplicación que necesita recuperarse de los fallos de OutOfMemoryError, y en los programas de un solo subproceso siempre funciona, pero a veces no en los programas de subprocesos múltiples. La aplicación es una herramienta de prueba automatizada de Java que ejecuta las secuencias de prueba generadas con la máxima profundidad posible en las clases de prueba. Ahora, la IU debe ser estable, pero el motor de prueba puede quedarse sin memoria mientras crece el árbol de casos de prueba. Manejo esto por el siguiente tipo de idioma de código en el motor de prueba:
boolean isOutOfMemory = false; // flag used for reporting try { SomeType largeVar; // Main loop that allocates more and more to largeVar // may terminate OK, or raise OutOfMemoryError } catch (OutOfMemoryError ex) { // largeVar is now out of scope, so is garbage System.gc(); // clean up largeVar data isOutOfMemory = true; // flag available for use } // program tests flag to report recovery
Esto funciona siempre en aplicaciones de subproceso único. Pero recientemente puse mi motor de prueba en un hilo de trabajo separado de la interfaz de usuario. Ahora, la falta de memoria puede ocurrir arbitrariamente en cualquiera de los hilos, y no me queda claro cómo atraparla.
Por ejemplo, hice que el OOME se produjera mientras los cuadros de un GIF animado en mi UI estaban siendo ciclados por un hilo propietario creado detrás de escena por una clase Swing que está fuera de mi control. Pensé que había asignado todos los recursos necesarios por adelantado, pero claramente el animador está asignando memoria cada vez que recupera la siguiente imagen. Si alguien tiene una idea sobre cómo manejar las OOMEs planteadas en cualquier hilo, me encantaría escuchar.