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!
Abraços a todos!