为了解决this problem,我构建了一个(非常)小的project,它正在复制它的一部分。它是一个使用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中。 有没有人有想法?
谢谢!