Desmistificando o java.lang.OutOfMemoryError: PermGen space

O meu objetivo com este tópico é criar uma referência para solução deste problema que atinge tanta gente. Sei que existem outros tópicos sobre o assunto no fórum do GUJ, mas todos que encontrei (e não foram poucos!) tratam o problema de forma superficial. Há mais de dois anos eu procuro encontrar uma resposta definitiva para este problema sem sucesso. Consegui adiar sua solução com paliativos, aumentando o espaço destinado a memória non-heap na JVM da Sun (-XX:MaxPermSize) e posteriormente trocando esta JVM pela da BEA (JRockit). Porém, hoje cheguei um ponto que não dá mais para conviver com este problema. Espero resolvê-lo de uma vez por todas e com o meu relato aqui ajudar outras pessoas para que evitem investir tanto tempo nisto.

O sistema que desenvolvo atualmente faz uso dos seguintes frameworks/API’s:

  • Struts 1.2
  • Hibernate 3.2
  • MySQL Connector 5.0.4
  • iText 2.0
  • log4j 1.2.15

Obs: As outras API’s são dependências dos itens dessa lista.

Completando a descrição do ambiente, este sistema roda no Tomcat 5.5.26 com Java 6 (JVM da BEA, JRockit).

Após tantos problemas com o PermGen space eu comecei a monitorar o uso dela. Após alguns dias de medição eu reparei que enquanto a memória heap oscilava bastante a memória PermGen (non-heap) apenas aumentava, nunca diminuia. Este aumento embora constante era bastante lento, porém, após operações de deploy e undeploy este aumento era considerável. A esta altura eu já sabia o tipo de objeto que era armazenado na memória non-heap, mas não conseguia entender porque estes não eram liberados após o undeploy. Boa parte das referências em português sobre o assunto falam apenas que para resolver o problema bastava aumentar a quantidade de memória, algo que todos que conviveram com isso sabem que isso apenas o adia. Uma das poucas exceções é a apresentação “Ferramentas e Técnicas para Resolução de Problemas em Desempenho” do Claudio Miranda (Summa Technologies) no JustJava 2007. Ele explica muito bem o que é cada área de memória do Java e fala sobre problemas e técnicas para solucionar problemas comuns. Graças a essa apresentação eu conheci o que acredito que seja a real causa do problema, os classloaders leaks. Pena que eu não pude assistir a apresentação, fiquei apenas com os slides, hehe. Os slides podem ser baixados aqui.

O meu entendimento sobre o problema aumentou consideravelmente após a leitura deste post no blog do Frank Kieviet. O texto é bem didático, explica de forma clara o que é um classloader leak e como ele é criado. Depois deste ele publicou outro post onde demonstra uma forma de solucionar o problema através do uso das ferramentas jmap e jhat (ambas presentes no JDK da Sun), utilizadas para gerar um dump da memória e analisá-lo, respectivamente. As ferramentas realmente são muito interessantes, no entanto, o exemplo onde ele as utilizada é bastante simples, fica fácil identificar onde ocorre o classloader leak. Em uma aplicação real, com centenas e centenas de classes, essa busca não é algo trivial, pois basta um classloader leak para que o todas as classes fiquem presas pelo classloader. Como o exemplo que classloader leak criado pelo Frank usava uma API de logging eu comecei a suspeitar dos frameworks e API’s que utilizo, poderia estar fazendo mal uso deles e criando um classloader leak acidentalmente.

Buscando mais referências sobre o assunto eu acabei chegando a um relato do bug no Bugzilla do Tomcat, Tomcat 5.0.16 leaks memory when a webapp is reloaded or stopped/started, algo que tem tudo a ver com o que estou pesquisando. Segundo a descrição, bastava o deploy/undeploy de uma aplicação de exemplo do Struts (o struts-blank) extremamente simples para reproduzir o problema. Uma pessoa escreveu um comentário e falou que o problema é causado pelo uso do Java Beans Introspector pelo Struts e que o Spring resolvia isso com o uso de um listener que invoca o método Introspector.flushCaches(). Fiz um teste com o listener do Spring e outro invocando o método em um ServletContextListener que eu já utilizava, ambos sem sucesso.

Eu procurei escrever aqui tudo que sei sobre o problema até o momento. Pretendo continuar estudando os resultados do jhat, acredito que esta linha de solução seja promissora. Se alguém souber de algum problema típico no uso de alguma das API’s que listei que cause classloader leaks, por favor, não deixem de falar! Eu voltarei a postar quando tiver algum progresso, espero que logo possa postar a minha solução! :slight_smile:

Abraços a todos!

Por acaso vc já tentou usar outro servidor? Jetty, Glassfish, etc, para ver se realmente não é um problema do Tomcat?

Trocar a JVM para versão 5 não ajuda?

Tive um problema rodando Thinlet com JVM 6. Com a 5 não ocorria o erro.

Humm, é uma boa testar isso! Eu baixei o jetty 6.1 mas não consegui fazer um “hot deploy” do sistema. Depois eu consegui fazer deploy reiniciando o jetty mas não consegui fazer o undeploy sem reiniciar, apaguei os arquivos em /contexts e /webapps mas o sistema continuava de pé. Estou procurando no Google uma forma de resolver isso, nunca havia mexido no jetty.

[quote=danieldestro]Trocar a JVM para versão 5 não ajuda?

Tive um problema rodando Thinlet com JVM 6. Com a 5 não ocorria o erro.[/quote]
O problema também acontecia na época que eu usava o Java 5. :frowning:

Valeu pela ajuda, pessoal! Qualquer novidade estamos aí!

Eu estou confiante que a análise dos resultados do jhat vai me ajudar, mas não estou conseguindo fazer consultas com OQL. É muito estranho, tento fazer as consultas de exemplo do help mas elas não funcionam, fico esperando minutos e minutos e ele nunca responde nem mesmo as consultas mais simples. :frowning:

Estou procurando uma forma de resolver isto, quando tiver uma novidade eu posto aqui. O meu objetivo é criar uma query para descobrir se há algum objeto que seja atingível por dois classloaders diferentes.

Abraços,

[quote=bonfarj]Eu estou confiante que a análise dos resultados do jhat vai me ajudar, mas não estou conseguindo fazer consultas com OQL. É muito estranho, tento fazer as consultas de exemplo do help mas elas não funcionam, fico esperando minutos e minutos e ele nunca responde nem mesmo as consultas mais simples. :frowning:

Estou procurando uma forma de resolver isto, quando tiver uma novidade eu posto aqui. O meu objetivo é criar uma query para descobrir se há algum objeto que seja atingível por dois classloaders diferentes.

Abraços,[/quote]

Alguma novidade amigo?
Encontrei esse tópico aki no GUJ http://www.guj.com.br/posts/list/69165.java#363559, daí vou tentar executá-lo para ver se resolve…

Abraço,

Até aonde eu sei, esse problema é quando o espaço para objetos permanente estoura, uma maneira de contornar esse problema é aumentar o heap dessa memória, ou tentar reduzir o númerod de objetos estáticos. Outra coisa, vi você mencionando sobre hot deploy, no OC4J por exemplo quando faço vários deploys seguidos esse erro ocorre, daí só dando o restart, isso pode estar acontecendo o mesmo com você.

Espero ter ajudado em algo :wink:

Eu não pude continuar porque entrei de férias. De qualquer forma, o que foi sugerido no outro tópico não resolve o problema, apenas o adia.

Abraços,

Olá pessoal.

Estou para colocar no ar uma intranet com diversas no Glassfish que terá um alto volume de acesso. Também estava com esse problema, e ontem a noite estive incessantemente atrás de uma solução, pois já estava me fazendo perder alguns fios cabelos e sono. Apos longa pesquisa pelo google, verifiquei que o problema passava pelas libs cglib e javassist e geração de bytecode na área de geração permanente da jvm, onde o garbage collector não atua.
Até então só tinha conseguido alguns parâmetros na jvm para aumentar a permgen, ou seja, simplesmente ganhar tempo, atrasando o estouro da mesma.

Depois de ler essa thread relacionada ao spring

e

dentre muitos outros, cheguei a 3 opções:

1 - Trocar o contêiner por uma aplicação não baseada em tomcat, tais como Geronimo+Jetty ou IBM WebSphere

2 - Mudar a jvm para JRockit ou a JVM da IBM

3 - Testar a atualização do Hibernate para a versão liberada esse mês 3.3.1.GA, juntamente com javaassist 3.4GA e por precaução cglib-2.2 , bem como usar os seguintes parâmetros na JVM
-Xms512m -Xmx1024m -XX: PermSize=256m -XX:MaxPermSize=512 -XX:+UseConcMarkSweepGC -XX:+CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled

A primeira opção achei não ser a mais adequada, pois já tinha uma boa quantidade de aplicações no glassfish testadas e homologadas, outra porque eu não queria perder o conjunto de recursos que este container me fornece. E também, no caso do WebSphere por questões de licenciamento.

A segunda opção também barrava em questões de licenciamento, visto que a empresa que trabalho está em fase final de licenciamento de todo software proprietário que utiliza. Seria apenas mais um custo.

Me restou a terceira opção que logicamente seria uma última esperança e, no meu ver, a mais plausível, tento em vista o “tempo de estrada” desse problema.

Bom, optei pela terceira alternativa e fiquei com muita ansiedade para constatar o resultado. Hoje pela manha cheguei no trabalho e fiz a atualização das libs e trabalhei normalmente, fazendo muitos deploys de aplicações com seam+hibernate+jsf e durante o dia todo não precisei reiniciar o Glassfish nem uma vez :wink: , considerando que sempre a cada 10 ou 15 deploys precisava reiniciar o conteiner.

Heheheheh, ao meu ver, está solucionado o problema (se ele não estiver esperando para “dar pau” amanhã).

Fica aqui minha colaboração, caso algum colega também faça a atualização dessas libs gostaria de pedir que postasse aqui a confirmação ou reprovação da sua eficacia na resolução do problema.

Um abraço a todos

Não investiguei direito, mas hoje 29/06/2009 e já utilizando versões bem mais maduras das bibliotecas citadas…o problema ocorre.
De qualquer forma pretendo me aprofundar mais nisso em busca de solucionar e posto qualquer novidade.

Não sei se você já viu, mas eu já tinha criado há algum tempo um tópico sobre o mesmo assunto:
http://www.guj.com.br/posts/list/92491.java

O netbeans possui as mesmas ferramentas do JHat, no profiler dele. Mas é milhares de vezes mais rápido e fácil de usar, especialmente se sua aplicação consome muita memória, ou tem muitas classes. Nós substituimos totalmente o uso do JHat por ele.

Ah blz…Vou lá conferir.
Valeu!!

Ao inicializar a jvm aumente o valor máximo de memória usado:

passe os argumentos:
-Xms192m -Xmx768m

No Eclipse:
Window -> Preference -> Java -> Installed JRE -> Selecione o JRE -> Edit
No campo Default VM Arguments coloque o argumento

No meu caso o jboss console mostrava que 512 MB eram usado para a JVM (por default) após forçar o parâmetro ficou com 768 MB e parou o erro de “permgen”.
Esse errro ocorre devido aos grande número de objetos estáticos que vão ocupando cada vez mais a memória, cada conteiner trata de forma diferente a limpeza rotineira destes objetos.

Algumas coisas:

1 - Evite ressussitar tópicos muito antigos.
2 - Esses argumentos não tem relação com o PermGen e, sim, com a heap.
3 - Mesmo que tivessem relação (se fossem PermSize e MaxPermSize), o autor do tópico já alertou seu uso só adia o problema e que está em busca de uma solução alternativa.
4 - Não é o conteiner que trata a limpeza dos objetos, é a JVM.

Opa pessoal, ví que o tópico é bem antigo e por estar sendo assolado pelo problema, gostaria de compartilhar com vocês minha situação e minhas dúvidas ainda não solucionadas.

Usamos o OC4J para realizar os Deploys da aplicação, tal problema nunca tinha aparecido até o sistema entrar em Produção, onde, obviamente, o número de usuários aumentou bastante.

Quando mandei o último Deploy da aplicação, o OutOfMemoryError deu logo na entrada da aplicação… Pois bem o que ocorre é que demora algumas semanas até dá o próximo e ao parar e iniciar o container tudo volta ao normal, até porque pelo que entendi, o classloader fica vazio… Não sei o tamanho da memória em produção, até porque não temos como acessar daqui e queria pedir essa solução como um último recurso, ou seja, pra ganhar tempo caso eu precise pra de fato resolver o problema.

Minhas dúvidas quanto ao erro são os seguintes:

1 - O classloader é o espaço onde a VM vai guardando tudo o que estatico da aplicação, tipo os .class, vars static, String, contantes, etc. ???

2 - Ele enche quando se aumenta o número de acessos ou quando se faz um novo Deploy ?? porque imaginei que fosse nesse momento que os .class fossem carregados.

3 - Terei que reduzir o uso de Strings desnecessárias, isso ajudaria em algo ?? Mudar versão de Framework tbm ??

Lendo os artigos que o Viny passou nesta mesma Thread, esclareceu muita coisa mas restaram as dúvidas acima.

Uso hoje uma pancada de jars, vou fazer a pesquisa por cada versão de cada um deles e retorno se encontrar alguma coisa relacionada aos mesmo e o PermGen.

Abs []

Uma alternativa e avaliar o recurso físico disponível simulando fadiga da aplicação em produção para verificar “gargalos” do sistema. Para isso uso o JMeter e o comando #top do Linux fazendo teste de stress, tenho alguns materiais que já usei em apresentações que podem ser útil.

Repositório dos materias de teste que produzo em conjunto com amigos que também trabalham na área:
http://code.google.com/p/testesautomatizados/

Você pode fazer checkout na SVN como anônimo.

Ferramenta JMeter:

http://jakarta.apache.org/jmeter/

Opa, valew Arllen… vou dar uma estudada na sua solução… Mas tipo… as minhas perguntas ?? estou certo em minha linha de raciocínio ??? Causa do classloader leak e momento em que o mesmo ocorre… São esses das minhas questões mesmo ???

Mais uma vez, valew pelo material…

Abs []

Fiz um monitor da JVM para moniturar uma aplicação web a distância. Para facilitar a instalação fiz na forma de um JSP mesmo com scriplets. O código não está elegante. Mas me deu uma análise dos picos de uso de heap durante o dia. Também vejo quando o parm genspace está chegando no limite. Não renomeie o nome da jsp.

ops um pequeno bug no gráfico, vai a versão correta.

Espero que seja útil