java.lang.OutOfMemoryError: PermGen space

Bom, no post passado eu falei sobre problemas de Heap Space. Neste post, eu explicarei como funciona a memória permanente do Java, e como podemos resolver os problemas que ocorrem com ela.

A memória permanente do Java (Permanent Generation), ou simplesmente PermGen, é onde fica alocado tudo aquilo que “supostamente” nunca se altera, ou seja, é permanente, Dentro desta lista, podemos citar objetos da própria classe, dados de reflexão, pool de Strings, etc.

Vamos dar uma olhada no código abaixo:

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

/**
*
* @author hallan
*/
public class Main {

    public static void main (String[] args) {

        List list = new ArrayList();
        String numero = new BigDecimal(1000).pow(1000).toString();
        for (int i = 0; i < 20000; i++)
            list.add( ( numero + i ).intern() );
    }
}

Este código utiliza o método intern() da classe String. Pra quem não conhece, o metodo intern verifica se esta String já está no pool de Strings do Java. Caso ela não esteja, aloca ela lá, e a próxima vez que uma String igual a essa for criada, será pega a sua referência. Isso é bom, pois assim não temos várias Strings iguais, tornando a execução mais rápida (pra quem não conhece a fundo a classe String e o pool de Strings do Java, recomendo dar uma olhada).

Resumindo, esse código coloca no pool de Strings um monte de Strings gigantes (20 mil pra ser mais exato). Ao executar:

Exception in thread “main” java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at testes.Main.main(Main.java:25)

Java Result: 1

Por quê?
Simples: o pool de Strings encontra-se na memória permanente do Java, e socar o pool de Strings desta forma simplesmente estoura o limite default do java para a PermGen (64 mb).

Claro que ninguém fica usando o método intern() da String desta forma idiota como mostrado. O problema com a memória permanente acontece normalmente em contêiners e servidores web, como Tomcat, JBoss e Glassfish.

Why?

Não é só o pool de Strings que fica na PermGen. Lá ficam os dados das classes e dados de reflexão, classloaders e etc. E é ai que mora o problema.

Classloader

Classloader nada mais é que uma classe que carrega arquivos .class de uma jar ou war. E quando fizemos o undeploy de uma aplicação no servidor, o conteiner “perde” a referência ao classloader da aplicação em questão e tudo deveria ser limpo, correto?

No mundo perfeito sim… mas nosso mundo não é perfeito.

Veja bem o código do Servlet abaixo:

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

public class ServletTeste extends HttpServlet {
    private static final String STATIC_STRING = "Exemplo";
    protected void doGet (HttpServletRequest request, HttpServletResponse response)
                          throws ServletException, IOException {
    }
}

Entenda o que acontece ao fazer o deploy deste servlet:
permgen_1

Acima, existe uma simplificação (máxima) do conteiner web que contém a referência do classloader que foi criado somente para esta aplicação e para a instância do serlvet, que irá atender as requisições doGet(), doPost(), etc. A String, por ser static, tem sua referência guardada pela classe (e não pelo objeto).

Pontos importantes:

– Como qualquer outro objeto, o mesmo possui uma referencia para seu objeto de class (.class), utilizando o getClass() é possível obter a mesma.
– Todo objeto de classe tem uma referência para seu Classloader (para chegar até lá, basta fazer em qualquer objeto .getClass().getClassLoader() ).
– Todo classloader guarda referência de todas as classes que o mesmo carregou.

Sendo assim, toda vez que um objeto que não seja o AppClassLoader referenciar um objeto carregado pelo AppClassLoader, este não sera coletado pelo GC.

Sério?

Sim.

no exemplo acima, ao fazer o undeploy, todos os objetos serão coletados. Observe:

permgen_3

Agora veja o exemplo abaixo:

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

public class ServletTeste2 extends HttpServlet {
    private static final String STATICNAME = "Exemplo2";
    private static final Level CUSTOM_LEVEL = new Level("test", 550) {}; // classe  anônima

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

Ao fazer o deploy:

permgen_2
Observe que a classe CUSTOM_LEVEL é anônima. Isto é necessário porque o construtor de Level é protected. Código do contrutor:

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

Aqui, known é um ArrayList estático de todos os tipos de Level.
Sendo assim, a classe Level tem uma referencia static para todos os objetos LEVEL que já existem, inclusive aquele da classe anônima. Como diria meu amigo Felipe, aí que está o “pulo do gato”!

E o que acontece ao fazer o undeploy?

Observe a imagem abaixo:
permgen_4

Apenas a instância do ServletTeste2 pode ser coletada. Por conter um referência de fora do AppClassloader, o CUSTOM_LEVEL não pode ser coletado, e este tem uma refencia da classe, que tem uma referência do AppClassloader, que tem uma referência de TODAS AS CLASSES CARREGADAS!

Se for parar pra pensar, faz sentido: existem classes de fora da aplicação (do conteiner) que ainda tem referência para as classes da aplicação. Mesmo com um undeploy, elas não serão limpas pelo GC por isso.

Conclusão:  referências de fora de um objeto da aplicação (classes carregadas pelo classloader) causam o famoso “memory leak”.

O mais difícil é controlar isso, pois muitas frameworks como JSF, Rich Faces e Hibernate fazem isso de forma que não há como evitar.

Qual a solução então? Não quero que meu JBoss comece a berrar a cada 3 undeploy/deploy de uma aplicação!

O segredo está em aumentar a memória permanente, pois 64mb para um servidor web é pouco. Isso pode ser feito com os parâmetros -XX:PermSize e -XX:MaxPermSize.

O parâmetro -XX:PermSize define a memória minima para a PermGen, e o -XX:MaxPermSize define a memória máxima.

Assim:
java -XX:PermSize=128m -XX:MaxPermSize=256m -jar Aplicacao.jar

Mas… e no Tomcat?
Dentro do catalina.sh (ou catalina.bat para windows) basta criar a variável CATALINA_OPTS ou JAVA_OPTS e adicionar os parâmetros:
CATALINA_OPTS=”-XX:PermSize=128m -XX:MaxPermSize=256m” ou
JAVA_OPTS=”-XX:PermSize=128m -XX:MaxPermSize=256m”

E no JBoss?
Dentro da pasta $JBOSS_HOME/bin, existe um arquivo chamado run.conf. Encontre a linha:
if [ “x$JAVA_OPTS” = “x” ]; then
JAVA_OPTS=“-Xms128m -Xmx512m -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000”
fi

e adicione ao JAVA_OPTS:
JAVA_OPTS=-Xms128m -Xmx512m -XX:PermSize=128m -XX:MaxPermSize=256m -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000″

Desta forma, podemos monitorar todas a regiões de memória com o JConsole ou Probe e definir exatamente o quanto é necessário com os parâmetros da JVM.

Espero ter ajudado!

Abraço!

Deixe um Comentário

11 Comentários.

  1. Opa!

    Excelente! Cara muito interessante esse post…

    You are Predator!!!!!

  2. Ei…tu não trabalha não!? uhauhauhahuahu

    Massa!! Parabéns!!!

  3. Felipe Trevisol

    Bom não é nenhuma surpresa para mim e tenho certeza de que não é para nossos amigos também que o Hallan é mais do que um excelente profissional ele está se mostrando um Java archeologist. Quantos de nós tinhamos a consciência que existia uma memória particionada do java ?
    Bom gostaria de dizer que meu currículo acaba de ganhar mais uma linha de peso por conta desse post.

    Parabéns Hallan !!!!!

  4. orra.. desisto.. tu não pertence mais a esse mundo.. =D

  5. Poiseh.. foram algumas noites em claro heheh 🙂

  6. legal e muito bom seu post, mas achei o comeco (os exemplos inclusive) muito semelhante ao -> http://codare.net/2007/01/11/java-solucionando-o-erro-de-permgen-space/ se foi usado como referencia, nada mais justo que citar, nao acha?

    • Na verdade não usei este site como referência não: era um site em inglês e, infelizmente, realmente não coloquei a referência – erro meu.

      Provavelmente tanto eu quanto o link que você citou usamos a mesma fonte.

      De qualquer forma obrigado pela atenção!

  7. Muito bom, foi bem didático! rsss

    andei pesquisando e encontrei esse post-> http://weblogs.java.net/blog/jjviana/archive/2010/06/09/dealing-glassfish-301-memory-leak-or-threadlocal-thread-pool-bad-ide

    O cara desenvolveu uma solução definitiva, sem precisar aumentar a memória, mas foi para o glassFish. Acredito que não seja muito diferente para o JBoss, Tomcat e outros.

  8. Marlem - Uberlândia MG

    muito bom!!
    ajudou bastante, obrigado

  9. Muito bom amigo, mas as imagens não estão funcionando. Tem como subir de novo?
    Queria entender de onde é a classe Level. Como o servlet “reconhece” Level.

Deixe um Comentário


NOTA - Você pode usar estesHTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>