Cuando capturar OutOfMemoryErrors

03/10/2006

Java OutOfMemoryError
¿Porqué querrías capturar un OutOfMemoryError? Si un OutOfMemoryError se genera por la JVM, no hay mucho que podamos hacer, así que ¿qué utilidad tiene el OutOfMemoryError?

Encontrarse con un OutOfMemoryError significa que el recolector de basura ya ha hecho todo lo posible para liberar memoria reclamando los espacios de memoria de los objetos que no tienen referencias fuertes. Si no pudo reclamar suficiente espacio, entonces también intenta obtener memoria del sistema operativo subyacente, a menos que el espacio de heap haya alcanzado el límite superior de la JVM fijado por el parámetro -Xmx (-mx en JVMs anteriores a Java 2). Así que si aparece el OutOfMemoryError significa que no hay mas espacio en el heap disponible que se pueda reclamar en este momento, y que o el sistema operativo no ha podido dar más memoria a la JVM o hemos alcanzado el límite superior de la JVM. En cualquier caso, no hay mucho que podamos hacer, así que ¿porqué querríamos capturar un OutOfMemoryError?

Las secciones siguientes describen unas situaciones especiales en las que puede ser útil capturara un OutOfMemoryError.

Expandiendo la Memoria y Determinando sus Limites

El espacio heap de la JVM es un área de memoria donde residen todos los objetos. Además de los objetos, el heap también puede albergar memoria reservada para el recolector de basura y algunas otras actividades de la JVM. El tamaño total del heap normalmente se fija por dos parámetros del ejecutable Java:

  • -Xms (-ms antes de Java 2) para especificar el tamaño inicial de heap cuando se arranca la JVM; y
  • -Xmx (-mx antes de Java 2) para especificar al tamaño máximo hasta el que puede crecer la JVM.

Si no se especifican estos parámetros, la JVM utiliza valores por defecto que varían, dependiendo de la versión y vendedor de la JVM. Los valores iniciales normalmentes on uno o dos megabytes, y los valores máximos por defecto típicamente se encuentran entre 16 y 64 Mbytes.

Obviamente, si controlas cómo se arranca la JVM, puedes especificar los valores de heap que desees. Pero ¿qué pasa si tu aplicación se ejecuta en una JVM que no ha sido arrancada bajo tu control? ¿Cómo puedes determinar el tamaño que la JVM pude alcanzar? Existe un método en la clase Runtime, Runtime.totalMemory(), que parece que nos proporcionaría dicha información, pero el valor devuelto es el tamaño actual de la memoria, no el tamaño máximo posible. En la versión 1.4 de Java existirá un nuevo método, Runtime.maxMemory(), que devuelve el valor de -Xmx, pero que no sirve para versiones anteriores de la JVM. Además, incluso si conocemos el valor pasado a -Xmx, no tenemos garantizado que la JVM pueda alcanzar el tamaño indicado, ya que pudo especificarse un tamaño demasiado grande para el sistema subyacente.

Una forma de determinar realmente el tamaño máximo posible del heap es hacer crecer el heap hasta que no se pueda expandir más. Hacer esto es bastante sencillo: creamos objetos hasta que alcanzamos el OutOfMemoryError. El siguiente método testMemory() crea repetidamente objetos de un megabyte de tamaño hasta que se genera un OutOfMemoryError:

public static final int MEGABYTE = 1048576;
public static long testMemory(int maximumMegabytesToTest)
{
// Contenedor de memoria, de lo contrario sería recolectada
Object[] memoryHolder = new Object[maximumMegabytesToTest];
int count = 0;
try
{
for (; count < memoryHolder.length; count++)
{
memoryHolder[count] = new byte[MEGABYTE];
}
}
catch(OutOfMemoryError bounded){}
long highWater = Runtime.getRuntime().totalMemory();
// System.out.println("Tamaño máximo en bytes: "
// + highWater);
// System.out.println("Tamaño reservable en
// megabytes: " + count);
memoryHolder = null; // liberar para el GC
// Sabemos que podemos reservar "count" megabytes y
// tenemos un valor máximo de "highWater". Devolvemos
// lo que prefiramos.
//return count;
return highWater;
}

El método devuelve el tamaño que ha alcanzado el heap cuando se generó el OutOfMemoryError. Sin embargo el uso de este método tiene sus consecuencias. Primero, aunque utilizamos un megabyte de tamaño para incrementar la petición de memoria, al tamaño real de memoria ocupada será mayor de un megabyte debido a que el objetos byte[] array tiene algo de tamaño adicional por ser un objeto. Segundo, si el heap está fragmentado, y el recolector de basura no puede o no defragmenta suficientemente el heap, realmente habrá mas espacio en el espacio heap para la creación de objetos, aunque nosotros no podamos crear otro objeto de un megabyte de tamaño. Finalmente, la JVM pude haber crecido tanto que ahora esté paginada por el sistema operativo (ver más abajo “Paginación del Sistema Operativo”), lo que causará un significante deterioro de rendimiento. Esto ocurriría de todos modos si la JVM necesita crecer lo bastante durante la ejecución normal del programa, pero la ejecución del método testMemory() impodría la sobrecarga primero.

Ninguno de estos puntos importan si estás interesado solamente en la marca superior (high-water mark), esto es, el tamaño máximo en que puede crecer el heap, pero puede ser importante en otras situaciones.

Limpiando Memoria

A menudo necesitamos realizar pruebas en código Java para determinar el rendimiento o coste de memoria. Cuando intentamos comparar dos métodos diferentes, debemos asegurarnos que las pruebas comienzan en el mismo entorno base para cada método, para realizar una comparación de igual a igual. Parte de esta igualación de entorno consiste en asegurarse que no existen otros procesos o hijos obteniendo recursos del sistema, que el sistema está ejecutando los procesos con las mismas prioridades, y que cualquier caché que se utilice está igual de cargada o vacía antes de comenzar cada método. Una parte de la igualación del entorno consiste en asegurarse que el sistema de ejecución comienza con la misma cantidad de memoria disponible para cada método. Sin esta igualación, es posible que la ejecución del primer método incurra en el coste de aumentar el tamaño del heap de la JVM (pidiendo trozos al sistema subyacente), o al revés, que el segundo método incurra en el coste de la recolección de basura cuando necesite memoria que fué reservada durante el primer método pero que aún fue reclamada.

Idealmente, la JVM proporcionaría un método que liberaría la memoria para mí. System.gc() parece que sería dicha llamada. Desafortunadamente, System.gc() solamente es una indicación para la JVM que indica que “ahora sería un buen momento para ejecutar el recolector de basura”. La JVM pude ignorar la indicación, o pude ejecutar una recolección de basura parcial, o una completa marca y liberación (mark-and-sweep) de todos los espacios, o cualquier cosa. En lugar de apoyarnos en el recolector de basuras, podemos adaptar el método de la sección anterior para limpiar la memoria. Esto lo podemos hacer reservando tanta memoria como sea posible, como en el anterior método testMemory(), y después liberando toda la memoria obtenida y solicitando un poco más. La última petición sirve para lanzar al recolector de memoria para que reclame inmediatamente toda la memoria que estábamos reteniendo. El método es el siguiente:

public static void flushMemory()
{
// Usamos un vector para almacenar la memoria.
Vector v = new Vector();
int count = 0;
// inicialmente incrementamos en bloques de 1 megabyte
int size = 1048576;
// Continuaremos hasta que pediremos bloques de
// 1 byte
while(size > 1)
{
try
{
for (; true ; count++)
{
// pedir y almacenar más memoria
v.addElement( new byte[size] );
}
}
// Si encontramos un OutOfMemoryError, seguimos
// intentando obtener más memoria, pero en bloques de
// la mitad de tamaño.
catch (OutOfMemoryError bounded){size = size/2;}
}
// Ahora liberamos todo para el GC
v = null;
// y pedimos un nuevo Vector como un pequeño objeto
// para asegurarnos que se lanza la recolección de basura
// antes de salir del método.
v = new Vector();
}

Esta es una solución independiente de la JVM para limpiar la memoria. Podríamos incluso utilizarlo en una aplicación si sabemos que tenemos varios segundos en un momento determinado cuando la aplicación no estuviese ejecutando nada más, pero no lo recomendaría. Aunque flushMemory() funcionaría en cualquier JVM, se trata de un proceso muy estresante y podría “romper” alguna JVMs. En concreto la JVM 1.2.0 de Sun en Windows “rompe” el hilo principal cuando se ejecuta flushMemory(), y deja el hilo del recolector de basura en un bucle si ejecutamos con el parámetro adicional -verbosegc.

Otras Situaciones

Como hemos visto, hay situaciones en las que capturar un OutOfMemoryError es útil. La mayoría de situaciones de ejecución en las que querríamos capturar un OutOfMemoryError involucrarían intentar continuar con el proceso en procesos monitores o servidores de larga-vida más allá de dicho error fatal. Es posible capturar el OutOfMemoryError si, haciéndolo, sabemos que podemos restaurar la aplicación a un estado en el que pueda continuar con el procesamiento. Por ejemplo, muchas aplicaciones tienen hilos ejecutándose en un estado de demonio. Este tipo de hilos son finalizados automáticamente por una JVM cuando todos los hilos que quedan ejecutándose son hilos demonio. Si tienen un hilo demonio, generalmente proporciona un sencillo servicio a una aplicación. Por ejemplo, un hilo temporizador podría simplemente fijar marcas de timeout en variables compartidas. En este tipo de casos, es posible capturar un OutOfMemoryError, ya que es relativamente sencillo regresar a un estado conocido, y el hilo pude continuar proporcionando su servicio. Otras aplicaciones son totalmente guiadas por eventos. Estas aplicaciones pueden capturar un OutOfMemoryError, desregistrar el event listener que lanzó el OutOfMemoryError, solicitar a todos los manejadores de caché que reduzcan la cantidad de memoria utilizada, y después simplemente continuar el proceso.

Sin embargo, para la mayoría de hilos, generalmente hay demasiadas cosas que podrían ir mal cuando se lanza un OutOfMemoryError. Mejor que capturar un OutOfMemoryError y otra serie de posibles objetos Error, normalmente es mejor la monitorización del proceso desde un proceso separado, o el uso de scripts de arranque en bucle, y automáticamente reiniciar si sucede un error fatal. De este modo, la aplicación reiniciada está en un estado conocido. Esto se puede hacer desde Java utilizando Runtime.exec() para iniciar el proceso y usar Process.waitFor() para monitorizar que está vivo. Si utilizamos esta técnica, nos debemos asegurar de leer continuamente la salida del Proceso (desde Process.getInputStream() y Process.getErrorStream()), o de lo contrario el proceso se podría bloquear en su salida, y aparentemente, no terminaría.

Paginación del Sistema Operativo

La paginación del sistema operativo ocurre cuando un programa es demasiado grande para entrar en la memoria real disponible (RAM), pero puede entrar en memoria virtual. La paginación mueve páginas del programa entre la RAM y el archivo de paginación de disco, permitiendo que el sistema operativo parezca que dispone de más memoria de la RAM disponible, pero a costa de rendimiento del programa.

Vía

About these ads

5 Responses to “Cuando capturar OutOfMemoryErrors”

  1. Nicolás Sánchez Says:

    Qué tal?. Sabes que tengo un problema con respecto al tema que comentas ( OutOfMemory error ). Estoy trabajando con Tomcat proceso que lo tengo configurado en los servicios de windows. El tema es el siguiente, ¿cómo puedo detectar que ha ocurrido un error de OutOfMemory y reiniciar automáticamente el servicio?. Dentro de los servicios, en los registros de windows, puedes configurar el tamaño del head configurando, por ejemplo, los siguientes valores:
    1- JVM Option Number :
    Información del Valor: ” -Xms512m ”

    2- JVM Option Number :
    Información del Valor: ” -Xmx1024m ”

    ¿Cómo puedo configurar para que se detecte un OutOfMemory y se reinicie automáticamente el servicio ?.

    Desde ya agradezco mucho tus comentarios.

    Un arbazo,
    Nicolás.-

  2. rob Says:

    Hola
    Nosotros hacemos una conexión a la página web, y si no aparece nada, reiniciamos el servidor web.
    Saludos

  3. alvaro Says:

    Relaize pruebas para el netbeans 5.5 y el netbeans 6.0
    aumentarle mas espacio de memoria pero realmente al momento de ejecutar un programa toma los valores por defecto, necesito mas memoria del heap memory para poder ejecutar mi aplicacion con gran volumenes de datos si sabes como hacerlo por favor hasmelo saber

  4. alvaro Says:

    Relize pruebas para el netbeans 5.5 y el netbeans 6.0
    aumentarle mas espacio de memoria pero realmente al momento de ejecutar un programa toma los valores por defecto, necesito mas memoria del heap memory para poder ejecutar mi aplicacion con gran volumenes de datos si sabes como hacerlo por favor hasmelo saber
    a mi correo neoman122@hotmail.com


  5. […] • Finalmente, temas de política que pueden provocar conflictos inter-departamentales: si una aplicación se cae y tira a la otra, ¿de quién es la culpa? ¿De la primera por afectar a la segunda o de la segunda por ser tan inestable que "se dejó tirar"? Este último escenario es común incluso en aplicaciones corriendo en diferentes contextos pero que conviven en la misma máquina, como las soluciones web al generar los temidos OutOfMemoryError. […]


Deja un comentario

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

Seguir

Recibe cada nueva publicación en tu buzón de correo electrónico.

A %d blogueros les gusta esto: