为了解决这个问题,我构建了一个(非常)小的项目来复制它的一部分。这是一个使用 Glassfish v2.1.1 和 OpenJpa-1.2.2 的 NetBeans 项目。
在全球范围内,目标是能够动态地重新加载一些业务代码(称为“任务”),而无需(重新)进行完整部署(例如通过 asadmin)。在项目中有两个:PersonTask 和 AddressTask,它们只是简单地访问一些数据并将它们打印出来。
为了做到这一点,我实现了一个自定义类加载器,它读取类文件的二进制文件并通过defineClass
方法注入它。基本上,这个 CustomClassLoader 是一个单例,实现如下:
public class CustomClassLoader extends ClassLoader {
private static CustomClassLoader instance;
private static int staticId = 0;
private int id; //for debugging in VisualVM
private long threadId; //for debugging in VisualVM
private CustomClassLoader(ClassLoader parent) {
super(parent);
threadId = Thread.currentThread().getId();
id = staticId;
++staticId;
}
private static CustomClassLoader getNewInstance() {
if (instance!=null) {
CustomClassLoader ccl = instance;
instance = null;
PCRegistry.deRegister(ccl); //https://issues.apache.org/jira/browse/GERONIMO-3326
ResourceBundle.clearCache(ccl); //found some references in there while using Eclipse Memory Analyzer Tool
Introspector.flushCaches(); //http://java.jiderhamn.se/category/classloader-leaks/
System.runFinalization();
System.gc();
}
ClassLoader parent = Thread.currentThread().getContextClassLoader();
instance = new CustomClassLoader(parent);
return instance;
}
//...
}
//this class is included in the EAR like a normal class
public abstract class AbstractTask {
protected Database database; /* wrapper around the EntityManager, filled when instance is created */
public abstract void process(Integer id);
}
//this one is dynamically loaded by the CustomClassLoader
public class PersonTask extends AbstractTask {
@Override
public void process(Integer id) {
//keep it empty for now
}
}
在我的 EJB 外观 (EntryPointBean) 中,我只需查找类,创建它的新实例并调用它的process
方法。项目中的代码略有不同,但思路大同小异:
CustomClassLoader loader = CustomClassLoader.getNewInstance();
Class<?> clazz = loader.loadClass("ch.leak.tasks.PersonTask");
Object instance = clazz.newInstance();
AbstractTask task = (AbstractTask)instance;
/* inject a new Database instance into the task */
task.process(...);
到现在为止,一切都很好。如果此代码多次运行(通过ch.leak.test.Test
),则在完成堆分析时将只有一个 CustomClassLoader 实例,这意味着之前的实例已成功收集。
现在,这是触发泄漏的行:
public class PersonTask extends AbstractTask {
@Override
public void process(Integer id) {
Person p = database.getEntity("SELECT p FROM Person p WHERE p.personpk.idpk=?1", new Long(id));
//...
}
}
这种对数据库的简单访问有一个奇怪的结果:第一次运行代码时,所使用的 CustomClassLoader 永远不会被垃圾收集(即使没有任何 GC 根)。但是,所有进一步创建的 CustomClassLoader 都不会泄漏。
正如我们在下面的转储中看到的(使用 VisualVM 完成),实例 id 为 0 的 CustomClassLoader 永远不会被垃圾收集......
最后,我在探索堆转储时看到的另一件事:我的实体在 PermGen 中声明了两次,其中一半没有实例,也没有 GC 根(但它们没有链接到 CustomClassLoader)。
似乎 OpenJPA 与这些泄漏有关......但我不知道在哪里可以搜索关于我做错了什么的更多信息。我还将堆转储直接放入项目的 zip 中。有人有想法吗?
谢谢 !