この問題を解決しようとして、その一部を再現する (非常に) 小さなプロジェクトを作成しました。Glassfish v2.1.1 と OpenJpa-1.2.2 を使用した NetBeans プロジェクトです。
グローバルな目標は、(たとえば asadmin を介して) 完全な展開を (再) 作成する必要なく、一部のビジネス コード (「タスク」と呼ばれる) を動的に再読み込みできるようにすることです。プロジェクトにはそのうちの 2 つ、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 のインスタンスは 1 つだけになります。これは、以前のインスタンスが正常に収集されたことを意味します。
ここで、リークをトリガーする行を次に示します。
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 はガベージ コレクションされません...
最後に、ヒープ ダンプを調べているときにもう 1 つ確認したことがあります。私のエンティティは PermGen で 2 回宣言されており、その半分にはインスタンスも GC ルートもありません (ただし、CustomClassLoader にはリンクされていません)。
OpenJPA がこれらのリークと関係があるようです...しかし、私が間違っていることの詳細情報をどこで検索できるかわかりません。また、ヒープ ダンプをプロジェクトの zip に直接入れました。誰にもアイデアはありますか?
ありがとう !