Java

La temida excepción «java.lang.OutOfMemoryError: PermGen space failure»

Java Garbage

¿Alguna vez te has encontrado con un error

java.lang.OutOfMemoryError: PermGen space failure

al re-desplegar tu aplicación en un servidor de aplicaciones? Acusaste al servidor de aplicaciones, mientras lo reiniciabas, para continuar con tu trabajo pensando que claramente se trata de un bug en el servidor de aplicaciones. Esos desarrolladores de servidores de aplicaciones deberían metérselo por donde les quepa, ¿o no? Bueno, quizás. Pero quizás ¡la culpa sea tuya!.

Veamos el siguiente ejemplo de un, aparentemente, inocente servlet.


package com.stc.test;

import java.io.*;
import java.util.logging.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class MyServlet extends HttpServlet {
  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // Log at a custom level
    Level customLevel = new Level("OOPS", 555) {
    };
    Logger.getLogger("test").log(customLevel, "doGet() called");
  }
}

Intenta re-desplegar este pequeño ejemplo varias veces. Apuesto a que en algún momento fallará con el temido error

java.lang.OutOfMemoryError: PermGen space failure

. Si quieres saber que está pasando continua leyendo.

Analizando el problema

Los servidores de aplicaciones como Glassfish te permiten crear una aplicación (.ear, .war, etc) y desplegarla junto con otras aplicaciones en este servidor de aplicaciones. Si ver necesario realizar un cambio en tu aplicación, sencillamente puedes realizar el cambio en tu código fuente, compilarlo, y re-desplegar la aplicación sin afectar al resto de aplicaciones que aún se están ejecutando en el servidor: no necesitas re-iniciar el servidor de aplicaciones. Este mecanismo funciona bien en Glassfish y otros servidores de aplicaciones.

El modo en que esto funciona es que cada aplicación se carga usando su propio classloader (cargador de clases). Esto es, un classloader es una clases que carga archivos .class. Cuando eliminas la aplicación, el classloader se descarta y él y todas las clases que ha cargado, deberían ser recolectadas como basuara entes o después.

De alguna forma, sin embargo algo podría hacer que se mantuviera el classloader, y evitar que fuese recolectado como basura. Y eso es lo que está causando la escepción «java.java.lang.OutOfMemoryError: PermGen space».

Espacio PermGen

¿Y qué es el espacio PermGen? La memoria de la Máquina Virtual se divide en varias regiones. Una de estas regiones es el PermGen. Es un area de memoria utilizada para (entre otras cosas) cargar archivos class (y ojo que son clases y no objetos -o instancias de dichas clases-). El tamaño de esta región de memoria es fijo, esto es, no cambia mientras la VM se esté ejecutando. Podemos especificar el tamaño de esta región con un parámetro de línea de comandos: -XX:MaxPermSize=XXXm . El valor por defecto es 64 Mb en la VMs de Sun.

Si hay algún problema con las clases que son recolectadas como basura y continuamos cargando clases, la VM se quedará sin espacio en esta región de memoria, incluso si hay un montón de memoria disponible en el heap. Fijar el parámetro -Xmx no ayudará: este parámetro solo especifica el tamaño total del heap y no afecta al tamapo de la región PermGen.

Recolección de basura y cargadores de clases

Cuando escribes algo tan estúpido como:


  private void x1() {
    for (;;) {
      List c = new ArrayList();
    }
  }

estás creando objetos continuamente; sin embargo el programa no se queda sin memoria: los objetos que creas son recolectados como basura y por tanto liberando el espacio de memoria de modo que puedas coloar otros objetos. Un objeto solamente puede ser recolectado como basura si el objeto es «inalcanzable». Lo que esto quiere decir es que no hay modo de acceder al mismo desde ninguna parte del programa. Si nadie puede acceder al objeto, no tiene sentido mantenerlo, y por tanto puede ser recolectado como basura. Echémosle un ojo a la imagen de la memoria del servlet de ejemplo. Primero, simplifiquemos aún mas el este ejemplo:


package com.stc.test;

import java.io.*;
import java.net.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class Servlet1 extends HttpServlet {

  private static final String STATICNAME = "Simple";

  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  }
}

Tras cargar el servlet anterior, en memoria tenemos los siguientes objetos (por supuesto limitándonos a los relevantes):

Mapa de memoria del sencillo servlet

En la imagen podemos ver los objetos cargados por el classloader de la aplicación en amarillo, y el resto en verde. Mostramos un objeto contenedor simplificado que contiene referencias al classloader de la aplicación creado únicamente para esta aplicación, y a la instancia del servlet de modo que el contenedor pueda invocar su método doGet() cuando llega una petición. Nota que el objeto de clase posee un objeto STATICNAME. Otras cosas importantes a notar son:

  1. Como cada objeto, la instancia Servlet1 mantiene una referencia a su clase (Servlet1.class).
  2. Cada objeto class (ej. Serlvet1.class) mentiene una referencia al classloader que lo cargó.
  3. Cada classloader mantiene referencias a todas las clases que ha cargado.

La consecuencia importante de esto es que siempre que un objeto de fuera del AppClassloader tenga una referencia a un objeto cargado por el AppClassloader, ninguna de sus clases pude ser recolectada como basura.

Para ilustrar esto, veamos que ocurre cuando la aplicación se elimina (undeploy): el objeto Container pone a null sus referencias a la instancia Servlet1 y al objeto AppClassloader.

Mapa de memoria tras desinstalar la aplicación

Como podemos ver, ninguno de los objetos es alcanzable, de modo que todos pueden ser recolectados como basura. Veamos ahora que ocurre cuando usamos el ejemplo original donde utilizamos la clase Level:


package com.stc.test;

import java.io.*;
import java.net.*;
import java.util.logging.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class LeakServlet extends HttpServlet {
  private static final String STATICNAME = "This leaks!";
  private static final Level CUSTOMLEVEL = new Level("test", 550) {
  }; // anon class!

  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    Logger.getLogger("test").log(CUSTOMLEVEL, "doGet called");
  }
}

Fíjate en que la clases CUSMTOMLEVEL es una clases anónima. Esto es necesario porque el constructor de Level es protegido. Echemos un ojo a la imagen de memoria de este escenario:

Mapa de memoria del servlet original

En la imagen podemos ver algo no esperado: la clase Level contiene un miembro estático de todos los objetos Level que han sido creados. Este es el constructor de la clase Level en el JDK:


  protected Level(String name, int value) {
    this.name = name;
    this.value = value;
    synchronized (Level.class) {
      known.add(this);
    }
  }

Aquí known es un ArrayList estático en la clase Level. Ahora, ¿qué ocurre si la aplicación es desinstalada?

Mapa de memoria tras desintalar el servlet original

Solamente el objeto LeakServlet puede ser recolectado como basura. Debido a la referencia del objeto CUSTOMLEVEL fuera del AppClassloader, el objeto de la clases anónima CUSTOMLEVEL (LeadServlet$1.class) no se puede recolectar como basura, y por tanto tampoco puede serlo el AppClassloader, y por tanto no puede ser recolectada como basura ninguna de las clases que cargó el AppClassloader.

Conclusión: cualquier referencia desde fuera de la aplicación a un objeto de la aplicación en la cual haya sido cargada la clase por el cargador de clases de la aplicación, causará una fuga de memoria en el cargador de clases.

vía

Algunos enlaces de interés

Java JVM GC, PermGen, and Memory Options
Preventing Java’s java.lang.OutOfMemoryError: PermGen space failure
Java Permgen space, String.intern, XML parsing 10
Good Riddance, PermGen OutOfMemoryError !
Understanding and avoiding the Java Permgen Space error
A Quick explication of JVM Memory Pools

22 respuestas a “La temida excepción «java.lang.OutOfMemoryError: PermGen space failure»

  1. Gracias por la iformacion.Mucho no entendi porque no tengo mucha experiencia .Me gustaria saber como solucionarlo ya que trabajo con el servidor Tomcat 5.5 y me esta dado el error. Muchas Gracias

  2. Umm esto me está sucediendo con una tarea y ya me tiene de las mechas. Entendí poco en realidad, pero em guastaria más bien como consejos de cuáles son las cosas que debemos evitar hacer para llegar a esto, qué cosas debemos minimizar??

  3. Te agradesco Infinitamente

    Agregue la siguiente linea en las propiedades generales de la jvm (Servidores de aplicaciones > Svr_Aplicacion > Definición de proceso > Máquina virtual Java) y dejo de pasmarse la aplicacion, el error hasta el momento ya no salio

    -XX:MaxPermSize=256m

  4. Excelente. Muy bien explicado, y muy útil. Es una de las cosas que a veces por ir demasiado atareados no nos paramos a intentar comprender.

    Sergio.

  5. este articulo es una traduccion del articulo original.

    tengo una duda, ¿el sentido de la referencia en rojo no deberia ser al reves? es decir, como yo lo veo, el objeto CUSTOMLEVEL hace referencia a la clase LEVEL.

Deja un comentario