Spring, Hibernate, Glassfish: Memory Leak beim Redeployment?
Java Januar 6th, 2011Eine in die Jahre gekommene Java Webanwendung, die mit dem Spring Framework (Web mvc) umgesetzt wurde, sollte ein wenig modernisiert werden. Vor allem die Umstellung auf Spring 3 und auf Annotationen basierende Controller war einiges an Migrationsarbeit.
Während des Testens viel mir auf, dass bei jedem Redeployment im Tomcat und im Glassfish 3 der Speicher knapper wurde. Nichts sonderlich ungewöhnliches bei Java Webanwendungen. Der beste Kandidat für ein sogenanntes Permgen-Leak ist dabei der MySQL Connector (Datenbank Treiber fuer MySQL), wenn dieser mit der Webanwendung im WAR Archiv liegt, und nicht vom Container bereitgestellt wird. Durch die Art, wie sich JDBC Treiber bei Java „anmelden“ können dann Speicherlecks entstehen, wenn die Webanwendung deaktiviert bzw. redeployt wird.
Das besonders nervige an Speicherlecks, die sich auf die Permanent Generation (Permgen) auswirken, ist dass eine einzige Referenz auf den Classloader reicht, um zu verhindern dass der Garbage Collector aufräumt. Das bedeutet, dass man den Erfolg seiner Maßnahmen erst dann sieht, wenn man alle Lecks abgedichtet hat: Eine echte Geduldsprobe.
Um hier etwas Zeit zu sparen und das Rad nicht neu zu erfinden habe ich mit die Errungenschaften des aktuellen Tomcat zur Nutze gemacht: Seit Tomcat 6.0.24 enthält dieser einen Memory-Leak-Prevention-Listener, der beim Beenden (undeploy) einer Webanwendung mögliche Memory Leak Probleme protokolliert – und in späteren Tomcat Versionen sogar bekämpft. (Details dazu auf http://wiki.apache.org/tomcat/MemoryLeakProtection )
Nachdem einige Kandidaten identifiziert waren, habe ich mich durch die Projekt Abhaengigkeiten im Maven POM gearbeitet. Mit Hilfe von Google, den vorhandenen Informationen von Tomcat und ein den genauen Versionen aus dem POM konnte ich dann bereits bekannte – und in neueren Versionen behobene – Leaks in Apache Commons Lang, Hibernate und Javassist ausmachen. Diese ließen sich zum Glück auch durch problemlose Updates beheben.
Was übrig blieb, war dann ein (intuitiv richtig diagnostiziertes) Problem mit Apache Commons Logging (hier gibt es dazu weitere Infos: http://wiki.apache.org/commons/Logging/UndeployMemoryLeak ). Um das Problem zu beheben, habe ich einen ServletContextListener erstellt, der dafür sorgt, dass die entsprechenden Ressourcen freigegeben werden.
Da ich eh dabei war, habe ich noch einen Cache Flush fuer den bei Reflection verwendeten Cache eingebaut. Das sieht dann so aus:
@Override public void contextDestroyed(final ServletContextEvent sce) { final Log logger = LogFactory.getLog(getClass()); logger.info("Releasing logger on shutdown."); LogFactory.release(Thread.currentThread().getContextClassLoader()); LogManager.shutdown(); Introspector.flushCaches(); }
Anschließend der re-test im Glassfish: Mehrfaches Deployment zeigte erneut Leaks in der PermGen. Hier darf sich nun ruhig etwas Frustration ausbreiten, schließlich hat man das Gefühl keinen Fortschritt gemacht zu haben.
Weiter gehts damit, einen Heap-Dump der JVM zu analysieren. Ich finde den Memory Analyzer in Eclipse dazu ein ausgezeichnetes Werkzeug. (Der Code dafür wurde von der deutschen SAP als Eclipse Contribution gespendet, und stammt aus der jahrelangen Erfahrung im Support großer Java Projekte bei SAP Kunden.) Selbst bei großen Heapdumps arbeitet der Eclipse Memory Analyzer schnell und geht mit den Ressourcen recht sparsam um. Auch die Unterstützung zum Finden von Leaks durch den Memory Analyzer ist benutzerfreundlich und funktional.
Mit Hilfe der Analyse ist nun auch der nächste Leak Kandidat gefunden: Glassfish selber!
In dem von mir analysierten Projekt kommt Hibernate zum Einsatz, was wiederum Javassist verwendet um Bytecode Enhancement durchzuführen. In älteren Versionen von Javassist gibt es einen Fehler der dazu führt, dass Referenzen auf den Classloader gehalten werden, so dass es zu einem Speicherleck kommt.
Glassfish selber verwendet Javassist für die eigene Infrastruktur (JBoss Weld als Java Context and Dependency Injection (CDI) Referenzimplementierung). Das Problem ist nun, dass das (alte) Javassist von Glassfish 3.0.1 für die Webanwendungen sichtbar ist, und da es in einem übergeordneter Classloader ist, wird diese defekte Javassist Version bevorzugt verwendet. (Details: https://issues.jboss.org/browse/WELD-570, http://java.net/jira/browse/GLASSFISH-12368 )
Die „Lösung“ kann nun so aussehen, dass man Hibernate freundlich darum bittet nicht Javassist, sondern die cglib für Bytecode Manipulationen zu verwenden. Dann tritt dies Problem nämlich nicht auf. Hibernate wertet dazu die Umgebungsvariable „hibernate.bytecode.provider“ aus, bei einer normalen (Desktop) Java Anwendung würde man dazu die Environmentvariable folgendermaßen setzen: -Dhibernate.bytecode.provider=cglib
Im Glassfish kann dies über die Administrationsoberfläche geschehen: Enterprise Server -> System Properties, hier dann „Add Property“:
Name: "hibernate.bytecode.provider"
Value: "cglib"
Hier das Ergebnis nach den Umstellungen:
Man kann deutlich sehen, wie der Speicherverbrauch bei jedem neuen Deployment anwächst – aber eben auch wieder freigegeben wird, wenn die Anwendung undeployt wird.
Weitere Probleme gibt es wohl bei Glassfish 3.0.1 und Verwendung von EJB mit CDI (ebenfalls im Zusammenhang mit Javassist und Weld) – hier wird das Weld Update das mit Glassfish 3.0.2 oder Glassfish 3.1 kommen wird Abhilfe schaffen.
Auch bei Verwendung von Security über EJB gibt es ein Problem: Hier werden Referenzen in ThreadLocal Variablen gehalten und nicht korrekt entfernt. Hier kann man sich – wieder mal – mit einem speziellen ServletContextListener behelfen:
public class ThreadLocalClearingContextListener implements ServletContextListener { public void contextInitialized(ServletContextEvent sce) { // } public void contextDestroyed(ServletContextEvent sce) { cleanThreadLocals(); } private void cleanThreadLocals() { final Thread[] threadgroup = new Thread[256]; Thread.enumerate(threadgroup); for (int i = 0; i < threadgroup.length; i++) { if (threadgroup[i] != null) { try { cleanThreadLocals(threadgroup[i]); } catch (Exception ex) { // } } } } private void cleanThreadLocals(final Thread thread) throws NoSuchFieldException, ClassNotFoundException, IllegalArgumentException, IllegalAccessException { final Field threadLocalsField = Thread.class.getDeclaredField("threadLocals"); threadLocalsField.setAccessible(true); final Class threadLocalMapKlazz = Class.forName("java.lang.ThreadLocal$ThreadLocalMap"); final Field tableField = threadLocalMapKlazz.getDeclaredField("table"); tableField.setAccessible(true); final Object fieldLocal = threadLocalsField.get(thread); if (fieldLocal == null) { return; } final Object table = tableField.get(fieldLocal); final int threadLocalCount = Array.getLength(table); for (int i = 0; i < threadLocalCount; i++) { final Object entry = Array.get(table, i); if (entry != null) { final Field valueField = entry.getClass().getDeclaredField("value"); valueField.setAccessible(true); final Object value = valueField.get(entry); if (value != null && value.getClass().getName().equals("com.sun.enterprise.security.authorize.HandlerData")) { valueField.set(entry, null); } } } } }
Ein Großteil dieser Work-Arounds werden – hoffentlich – mit dem Update von Glassfish (entweder 3.0.2 oder 3.1, je nachdem welches Oracle eher freigibt) hinfällig. Leider ist es bei Oracle unüblich darüber Angaben zu machen wann – und ob – ein Update zu erwarten ist.
Als Alternative bliebe noch der gerade in Version 6 freigegebene JBoss Application Server. Hier kommt (natürlich) auch das JBoss Weld zum Einsatz, jedoch in einer neueren Version.
Neue Kommentare