PermGen Out of Memory

Si te encuentras con este error es porque probablemente estés trabajando en una aplicación enterprise bastante grande. El principal problema de este error es que la JVM no muere, por lo que es cuestión de suerte saber que algún nodo de nuestro sistema ha dejado de funcionar.

Intentemos entender lo que es el espacio PermGen. Primero debemos entender sobre los tres mecanismos de recolección del GC de Java:
  • Young
  • Tenured
  • Perm
Podéis leer más sobre estos mecanismos aquí. El Perm(anent) se caracteriza por responsabilizarse del código binario de clases y métodos. De esta forma se separan las instancias de los objetos, del código que los describe. Por lo que si la JVM llena este espacio por completo, no podremos crear instancias nuevas de objetos que no lo hayan sido anteriormente ya que sus firmas no podrán ser metidas en el PermGen.

Dado que cualquier aplicación utiliza hoy en día Spring, Hibernate y cuarenta librerías más, el espacio para PermGen se puede llenar bastante rápido sino tenemos cuidado. Antes de solucionar este problema, vamos a intentar reproducirlo.

Si nuestra aplicación se ejecuta en algún servidor de aplicaciones, lo más sencillo será reducir el tamaño máximo del PermGen al mínimo, tal que el servidor funcione, despliegue nuestra aplicación pero falle con la primera petición. Esto lo logramos variando los valores del argumento para la JVM MaxPermSize.

A continuación vamos a medir esto utilizando jconsole ya que es bastante gráfico. Lo primero será activar el servicio JMX de la JVM añadiendo los siguientes argumentos:
  • com.sun.management.jmxremote.port=12346"
  • com.sun.management.jmxremote.authenticate=false"
  • com.sun.management.jmxremote.ssl=false"
Arrancamos el servidor de aplicaciones y jconsole.

A continuación realizamos una petición a nuestra aplicación y podemos ver como aumenta el uso del PermGen:


Y continuamos hasta que alcanzamos el límite dado (64m):


Después de esto, jconsole se desconecta y no podremos seguir analizando, aunque nuestra aplicación sigue funcionando y es posible que no se pierda funcionalidad, pero es inestable y en el momento en el que se intente crear una instancia de un objeto cuya "firma" no se encuentra en el PermGen, fallará.

La solución inmediata a este problema es claramente aumentar la cantidad de memoria disponible para el PermGen y por ende, de la JVM o seguir analizando el problema. Volvamos a arrancar el servidor de aplicaciones y nos conectamos con jconsole. Hacemos una petición:


¡Casí! Vamos a pulsar el botón "Perform GC" a ver qué sucede:


¿Nada? ¿No se limpia el PermGen? Dado el nombre de este espacio de memoria era bastante obvio el resultado. Otra solución que encontramos en Internet es añadir la siguiente opción a la JVM -XX:+CMSClassUnloadingEnabled veamos qué sucede:


Nada, seguimos igual. Probemos con otra de las soluciones -XX:+CMSPermGenSweepingEnabled



De igual manera parece que no ha habido ningún cambio favorable y el rendimiento se ha visto impactado negativamente y la misma petición ha tomado el triple de tiempo en ser procesada.


Puede que sencillamente, nuestra aplicación en realidad necesita tal cantidad de memoria ya que utilizamos una cantidad ingente de librerías. Como se puede ver en el siguiente gráfico, después de una larga sesión de stress, la memoria PermGen se mantiene estable:


Esto es un servidor de aplicaciones el cual utiliza gran cantidad de librerías, contextos y distintas complejidades que pueden hacer que ciertas referencias se mantengan y eviten que el GC haga su trabajo, por lo que vamos a intentarlo con una aplicación J2SE. Esta es sencillamente un servicio REST que nos permite hacer búsquedas contra unos EJB's. Para hacer pruebas es muy útil y como podemos ver, su funcionamiento es similar al servidor de aplicaciones:


Se utiliza cierta cantidad de memoria del PermGen y después de varias peticiones, esta se mantiene estable, incluso después de añadir las posibles soluciones indicadas más arriba no hay ningún cambio.

Dado que no hay un crecimiento continuo y que éste se detiene una vez hayamos pasado por todas las posibles combinaciones y usos de nuestra aplicación, es posible indicar un valor máximo para el PermGen que sea lo bastante como para sostener nuestras aplicaciones y justo en el consumo de memoria.


Redesplespliegues
Ahora hablemos de nuestro amigo el ClassLoader. Un servidor de aplicaciones como Tomcat o JBoss utilizan un artilugio que se encarga de cargar los ficheros .class de los JARs que componen nuestra aplicación. 



Los ClassLoader tienen una jerarquia, donde el servidor de aplicaciones tiene uno o varios "propios" y por debajo de estos tenemos a los que se crean para cada una de nuestras aplicaciones. Dentro de ésta jerarquía las clases de nuestro ClassLoader pueden utilizar las de arriba, pero no al revés. Por lo tanto, al crear un nuevo objeto, sucede lo siquiente:
  • Cada objeto mantiene una referencia a su objeto .class.
  • Cada objeto .class mantiene una referencia con su ClassLoader.
  • Cada ClassLoader mantiene una referencia para cada clase que ha cargado.
Dado que cada una de nuestras aplicaciones tiene su ClassLoader, al volver a desplegar se destruyen todas las referencias al anterior para crear uno nuevo. Pues va a ser que no:



El ClassLoader vuelve a ser creado y por ende todas las referencias a clases antiguas se pierden y se crean nuevamente, pero no se realiza ninguna limpieza del PermGen... ever! Si continuamos redesplegando nuestra aplicación y haciendo peticiones, volveremos a quedarnos sin Perm.

Incluso si eliminamos el despliegue, el Perm no es liberado.


Soluciones
Podemos concluir que el PermGen nunca se libera de forma significativa y que debemos controlar su uso siguiendo estos consejos:
  • La solución más rápida es aumentar su tamaño máximo. 
  • Ordenar y desarrollar nuestras aplicaciones de tal forma que se maximice el uso de las librerías provistas por el servidor de aplicaciones.
  • Utilizar librerías comunes entre distintos proyectos y añadir dichas librerías al ClassLoader del servidor de aplicaciones. Un buen ejemplo son los conectores JDBC.
  • Evitar referencias externas a nuestra aplicación a objetos internos de la misma (ClassLoader Leak).
Solución JRockit

JRockit, la máquina virtual de Oracle que era de BEA y utilizada por defecto por WebLogic no implementa un PermGen, por lo que este problema no existe y el GC recolecta de forma normal siempre que se eliminen las referencias, por lo que si la aplicación contiene un leak de este tipo, tendremos un Out of Memory del HEAP que son más fáciles de ver y monitorizar.



Nota final
Mientras he estado realizando las distintas pruebas, he podido comprobar que al añadir la opción CMSPermGenSweepingEnabled el rendimiento baja significativamente, incluso en una aplicacion J2SE por lo que no lo recomiendo para nada.


Más información
http://www.jroller.com/agileanswers/entry/preventing_java_s_java_lang
http://www.oracle.com/technetwork/java/gc-tuning-5-138395.html
http://www.alessandroribeiro.com/?q=en/node/39
http://stackoverflow.com/questions/88235/how-to-deal-with-java-lang-outofmemoryerror-permgen-space-error
http://blogs.sun.com/fkieviet/entry/classloader_leaks_the_dreaded_java
http://docs.jboss.org/jbossas/jboss4guide/r2/html/ch2.chapter.html