Hibernate version: 3.2.6.GA (Hibernate Search 3.0.0.GA)
There is a ThreadLocal in org.hibernate.search.impl.SearchFactoryImpl that doesn't get cleaned up properly. This cause problems for hot-deploying to an application server if the hibernate jars are in the war file. When that's the case the ClassLoader cannot be garbage collected because SearchFactoryImpl.contexts is a ThreadLocal which lives in the threadLocal map inside the Thread instance for the current thread which was loaded (most likely) by the system classloader. However if hibernate is in the war then the SearchFactoryImpl class will be loaded by the WebAppClassloader and the SearchFactoryImpl.context is a static field and keeps SearchFactoryImpl.class from being collected which keeps WebAppClassloader around. This in turn keeps every class loaded in the war which will cause the JVM to run out of permgen space quickly.
A workaround is to use reflection to manually remove the threadlocal:
Code:
Field field = SearchFactoryImpl.class.getDeclaredField("contexts");
field.setAccessible(true);
ThreadLocal<?> threadLocal = (ThreadLocal<?>) field.get(null);
threadLocal.remove();
Looking at the source for SearchFactoryImpl I can see that the ThreadLocal contexts has a WeakHashMap stored and the set and get methods are called on the ThreadLocal but nothing is ever done to remove the ThreadLocal when it's no longer needed.
Note that people that use the hibernate that's in the %JBOSS_HOME%\lib folder won't have this problem. Or rather, they won't notice this problem because hibernate is loaded by the same classloader as the rest of JBoss and doesn't get unloaded when a war is hot-deployed. However that's not a satisfactory solution because our application (like many others I assume) needs to be able to be deployed on a number of application servers and we cannot count on them all having hibernate built in. Also we want to control which version of hibernate is being used so our persistence code doesn't break.
Here's some method calls to get to where this SearchFactoryImpl.context is created. Maybe this will help in figuring out when the ThreadLocal should be removed:
org.hibernate.search.impl.SearchFactoryImpl.getSearchFactory()
org.hibernate.search.event.FullTextIndexEventListener.initialize()
org.hibernate.cfg.search.SearchConfiguration.enableHibernateSearch()
org.hibernate.cfg.AnnotationConfiguration.buildSessionFactory()
org.hibernate.jmx.HibernateService.buildSessionFactory()
org.hibernate.jmx.HibernateService.start()
Looking at what happens when HibernateService.stop() is called, there's nothing that gets back to the SearchFactoryImpl to remove that ThreadLocal.
EDIT: It should be noted that while I'm calling this problem a memory leak, by Sun's definition this isn't a leak because it is not unbounded over time. One hot-deploy / undeploy causes the permgen space to grow by a fixed amount. As long as you don't redeploy, you could still run forever. However if you set up a commandline loop to say ... touch the war file every 40 seconds and do that about 100,000 times in a row ... it will crash with a permgen space out of memory exception after about 5-10 reploys (depending on the size of the war file). So why this technically isn't a "leak" per se, it behaves like a leak under the right circumstances. Just thought I'd add that :)