1

为了解决这个问题,我构建了一个(非常)小的项目来复制它的一部分。这是一个使用 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 永远不会被垃圾收集...... CustomClassLoader 没有被垃圾收集

最后,我在探索堆转储时看到的另一件事:我的实体在 PermGen 中声明了两次,其中一半没有实例,也没有 GC 根(但它们没有链接到 CustomClassLoader)。 在此处输入图像描述

似乎 OpenJPA 与这些泄漏有关......但我不知道在哪里可以搜索关于我做错了什么的更多信息。我还将堆转储直接放入项目的 zip 中。有人有想法吗?

谢谢 !

4

0 回答 0