¿Captando java.lang.OutOfMemoryError?
Documentación 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 detectar.
Pero como java.lang.Error
es una subclase de java.lang.Throwable
, puedo capturar este tipo de Throwable.
Entiendo por qué no es una buena idea detectar este tipo de excepción. Hasta donde tengo entendido, si decidimos capturarlo, el controlador de captura no debería asignar ninguna memoria por sí solo. De lo contrario OutOfMemoryError
será arrojado nuevamente.
Entonces, mi pregunta es:
- ¿Existen escenarios del mundo real en los que atrapar
java.lang.OutOfMemoryError
podría ser una buena idea? - Si decidimos catch
java.lang.OutOfMemoryError
, ¿cómo podemos asegurarnos de que el controlador catch no asigne memoria por sí solo (alguna herramienta o mejor práctica)?
Hay una serie de escenarios en los que es posible que desee detectar una JVM OutOfMemoryError
y, según mi experiencia (en JVM de Windows y Solaris), muy raramente es OutOfMemoryError
la sentencia de muerte para una JVM.
Solo hay una buena razón para detectar un error OutOfMemoryError
y es cerrar con elegancia, liberar recursos de manera limpia y registrar el motivo del error lo mejor que pueda (si aún es posible hacerlo).
En general, esto OutOfMemoryError
ocurre debido a una asignación de memoria de bloque que no puede satisfacerse con los recursos restantes del montón.
Cuando se Error
lanza, el montón contiene la misma cantidad de objetos asignados que antes de la asignación fallida y ahora es el momento de eliminar referencias a objetos en tiempo de ejecución para liberar aún más memoria que pueda ser necesaria para la limpieza. En estos casos, incluso puede ser posible continuar, pero definitivamente sería una mala idea ya que nunca se puede estar 100% seguro de que la JVM esté en un estado reparable.
Demostración de que OutOfMemoryError
no significa que la JVM se haya quedado sin 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 ejecuto algo crítico, generalmente capto el archivo Error
, lo registro en syserr, luego lo registro usando el marco de registro de mi elección, luego procedo a liberar recursos y lo cierro de manera limpia. ¿Qué es lo peor que puede pasar? La JVM está muriendo (o ya está muerta) de todos modos y, al detectarla, Error
existe al menos una posibilidad de limpieza.
La advertencia es que debe centrarse en detectar este tipo de errores sólo en lugares donde sea posible la limpieza. No mantas catch(Throwable t) {}
por todos lados ni tonterías así.
Puedes recuperarte de él:
package com.stackoverflow.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ías inicialmente tanta memoria? ¿También está bien tener menos memoria? ¿Por qué no lo utilizas ya de todos modos? O si eso no es posible, ¿por qué no darle más memoria a la JVM desde el principio?
Volviendo a tus preguntas:
1: ¿Existe algún escenario real en el que detectar java.lang.OutOfMemoryError pueda ser una buena idea?
No se me ocurre ninguno.
2: si detectamos java.lang.OutOfMemoryError, ¿cómo podemos estar seguros de que el controlador de captura no asigna memoria por sí solo (alguna herramienta o mejores prácticas)?
Depende de lo que haya causado el OOME. Si se declara fuera del try
bloque y sucedió paso a paso, entonces sus posibilidades son pocas. Es posible que desees reservar algo de espacio en la memoria de antemano:
private static byte[] reserve = new byte[1024 * 1024]; // Reserves 1MB.
y luego configúrelo en cero durante OOME:
} catch (OutOfMemoryException e) {
reserve = new byte[0];
// Ha! 1MB free!
}
Por supuesto, esto no tiene ningún sentido;) Simplemente proporcione a JVM suficiente memoria según lo requiera su aplicación. Ejecute un generador de perfiles si es necesario.
En general, es una mala idea intentar detectar y recuperarse de un OOM.
También se podría haber lanzado un OOME en otros subprocesos, incluidos subprocesos que su aplicación ni siquiera conoce. Cualquier hilo de este tipo ahora estará muerto, y cualquier cosa que estuviera esperando una notificación podría quedarse atascada para siempre. En resumen, su aplicación podría fallar terminalmente.
Incluso si se recupera con éxito, su JVM aún puede estar sufriendo de falta de almacenamiento dinámico y, como resultado, su aplicación funcionará abismalmente.
Lo mejor que se puede hacer con un OOME es dejar morir la JVM.
(Esto supone que la JVM muere . Por ejemplo, los OOM en un hilo de servlet de Tomcat no matan la JVM. Se sabe que esto hace que Tomcat entre en un estado catatónico en el que no responderá a ninguna solicitud en absoluto. Ni siquiera solicitudes en el puerto de administración que le indican que se reinicie solo).
EDITAR
No estoy diciendo que sea una mala idea captar OOM en absoluto. Los problemas surgen cuando intentas recuperarte del OOME, ya sea de forma deliberada o por descuido. Siempre que detecte un OOM (directamente, o como un subtipo de Error o Throwable), debe volver a ejecutarlo o hacer que la aplicación/JVM salga.
Aparte: esto sugiere que para obtener la máxima solidez frente a los OOM, una aplicación debería usar Thread.setDefaultUncaughtExceptionHandler() para configurar un controlador que hará que la aplicación se cierre en caso de un OOME, sin importar en qué subproceso se lance el OOME. Me interesaría opiniones sobre esto...
El único otro escenario es cuando sabes con certeza que el OOM no ha provocado ningún daño colateral; es decir, ya sabes:
- qué causó específicamente el OOME,
- qué estaba haciendo la aplicación en ese momento, y que está bien simplemente 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 se puede saber con certeza si la continuación después de un OOME es segura. Incluso si empíricamente "funciona" cuando lo pruebas.
(El problema es que se requiere una prueba formal para demostrar que las consecuencias de los OOME "anticipados" son seguras y que los OOME "imprevistos" no pueden ocurrir dentro del control de un OOME try/catch.)
Definitivamente hay escenarios en los que atrapar un OOME tiene sentido. IDEA los detecta y muestra 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 detectarlos e informarlos. La clave para hacer esto es hacerlo en un nivel alto en el envío para que tenga una posibilidad razonable de tener una gran cantidad de recursos liberados en el punto en el que detecta la excepción.
Además del escenario IDEA anterior, en general la captura debe ser de Throwable, no solo de OOM específicamente, y debe realizarse en un contexto donde al menos el hilo finalizará en breve.
Por supuesto, la mayoría de las veces la memoria se muere de hambre y la situación no es recuperable, pero hay maneras en que tiene sentido.
Me encontré con esta pregunta porque me preguntaba si es una buena idea detectar OutOfMemoryError en mi caso. Estoy respondiendo aquí en parte para mostrar otro ejemplo más de cuándo detectar este error puede tener sentido para alguien (es decir, yo) y en parte para descubrir si es una buena idea en mi caso (siendo yo un desarrollador súper junior, nunca podré estar demasiado seguro acerca de cualquier línea de código que escriba).
De todos modos, estoy trabajando en una aplicación para Android que se puede ejecutar en diferentes dispositivos con diferentes tamaños de memoria. La parte peligrosa es decodificar un mapa de bits de un archivo y mostrarlo en una instancia de ImageView. No quiero restringir los dispositivos más potentes en términos del tamaño del mapa de bits decodificado, ni puedo estar seguro de que la aplicación no se ejecutará en algún dispositivo antiguo con el que nunca me he encontrado y que tenga muy poca memoria. Por eso 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 consigo ofrecer dispositivos más y menos potentes según sus necesidades y expectativas, o más bien las de sus usuarios.