Java のクラスアンロード (Class Unloading)

作成日:2004.05.18
更新日:2005.06.21

今後、随時書き足して行きます。多分。

はじめに

Java は動的にクラスのロードとアンロードが行われる仕組みになっている。 クラスはクラスファイルの形でディスク上やネットワークに配置され、プログラム中でそれらが本当に必要になった段階で JavaVM 上に読み込まれる。 またクラスはその使用が終わった段階でガーベージコレクターによって動的に回収され JavaVM からアンロードされる。

Servlet / J2EE サーバーなどはこの性質を利用して運用中にプログラムの一部を入れ替えるホットスワップ (Hot Swap) を実現している。 だがこの仕組みを実装するには少し工夫がいる。

この文書ではクラスのアンロードを実現するやり方について述べる。

1. クラスのロードとアンロードの基本的な仕組み

クラスローダー

Java VM がクラスを読み込む際にはクラスローダー (Class Loader)が重要な役割を果たす。 イメージとしてはクラスを載せておく容器のようなものである。 オブジェクト(java.lang.Object) がクラス(java.lang.Class) に所属するように、クラスは何らかのクラスローダー(java.lang.ClassLoader)に所属している。

Java VM は起動時直後には、ブートストラップクラスローダー (Bootstrap Class Loader) と呼ばれるデフォルトのクラスローダーが 1 つだけ存在する。 このクラスローダーは、java. からはじまるパッケージのクラスなど CLASSPATH を指定しなくてもロード可能な特別なクラスを読み込むためにある。

ブートストラップクラスローダーによって読みこまれたクラスは、Class.getClassLoader() の値が null になる。

Object  anObject    = new Object();
Class   objectClass = anObject.getClass(); 

System.out.println(objectClass.getClassLoader());  // null となる
System.out.println(Object.class.getClassLoader()); // これも null
J2SE1.3 以降はブートストラップクラスローダーが読み込むのは jdk/jre に含まれる rt.jar になる。 別のクラスパスを指定したい場合は -Xbootclasspath:path オプションを指定することで変更することが可能。
また -Xbootclasspath/p:path オプションを指定するとブートストラップクラスローダーが -Xbootclasspath の前に読み込むクラスファイルを指定できる。 -Xbootclasspath/a:path オプションを指定すると -Xbootclasspath の後に読み込むクラスファイルも指定できる。

もう一つ システムクラスローダー (system class loader) と呼ばれるクラスローダーが暗黙のうちに作成される。 システムクラスローダーは CLASSPATH の位置に置かれたクラスを読み込むために使用される。 起動クラスやユーザーが書いたクラスファイルはシステムクラスローダーで読み込まれることになる。

システムクラスローダーがどのような型を持つかは言語仕様では決まっていないが、SUN、IBM、BEA などの JavaVM の実装では、rt.jar 内に入っている sun.misc.Launcher.AppClassLoader クラスがデフォルトのシステムクラスローダーとなる。

class  Test {
  public static void main(String[] args) {
    //   
    System.out.println(Test.class.getClassLoader());
  }
}

これを実行した場合、sun.misc.Launcher$AppClassLoader@1a5ab41 のように表示される(数字の部分は実行のたびに変化する)。 このインスタンスは ClassLoader.getSystemClassLoadser() の戻り値と一致する。

システムクラスローダーは、java.system.class.loader プロファイルを設定することで独自のものに変更することができる。
独自定義したシステムクラスローダーのクラスファイルはブーストラップクラスローダーから読み込み可能な位置に置く必要がある。

それ以外のクラスローダーは java.lang.ClassLoader クラスを 派生させ、そのインスタンスを生成することで作成する(ClassLoader は抽象クラスなので直接生成できない)。 また java.net.URLClassLoader などライブラリが用意したクラスローダークラスも使用できる。

独自に生成したクラスローダー上にクラスを読み込みたい場合は loadClass(String className) メソッドを試みる。 下のプログラムは 2 つの独自定義したクラスローダーを作成し、jp.nminoru.Hoge というクラスをそれぞれ別々に読み込もうとしている。
ただし、後述のように jp.nminoru.Hoge クラスが必ずしも aLoader 上に読み込まれるとは限らない。

MyClassLoader aLoader1 = new MyClassLoader();
MyClassLoader aLoader2 = new MyClassLoader();

// 同じクラス名でも別々のものとみなされる。
Class aClass1 = aLoader1.loadClass("jp.nminoru.Hoge");
Class aClass2 = aLoader2.loadClass("jp.nminoru.Hoge");

親への委譲

クラスローダーは下の図のように親子関係を持つ。
この親子関係は ClassLoader クラスからの派生関係とは関係なく、インスタンスレベルのものである。 下の図で言うと Derived Class Loader 1 〜 3 が同じ MyClassLoader 型のインスタンスということもある。
ブートストラップクラスローダー以外のクラスローダーは各 1 つづつ親クラスローダーを持っている。 新しいクラスローダーはデフォルトでは自分を作成したクラスのクラスローダーを親とする。 ブートストラップクラスローダーはルートとなり、親を持たない。

クラスとクラスローダー

このようなクラスの親子関係はクラス検索の 委譲モデル のために利用される。

上のプログラムでは MyClassLoader のインスタンス loader クラスローダーを作成し jp.nminoru.Hoge クラスを読み込ませた。 だが、loader の親が jp.nminoru.Hoge クラスを解決できる場合には、jp.nminoru.Hoge クラスは loader ではなく親クラスが読み込むことになる。

委譲モデルは loadClass(String name)loadClass(String name, boolean resolve) をオーバーライドすることで破壊することができるが推奨されない。 というか絶対にやっては駄目。
Java VM は (ClassLoader クラスを用いない) 暗黙のクラスロードに loadClass(String name) を使い、loadClass(String name) は単純に loadClass(String name, boolean resolve) を呼び出している。
そのため loadClass を書き換えると親クラスローダーへの委譲が大変に面倒になって、回避不能なバグが生じる可能性大。

クラスのアンロード

クラスのアンロードは、そのクラスが不要になった時に起こる。 クラスが不要になる条件は、以下の 3 つの条件を全て満たす必要がある。

  1. ヒープ中からそのクラスのインスタンスがなくなること。
  2. そのクラスの static メソッドを実行中のスレッドがいないこと。
  3. そのクラスをロードしたクラスローダーを現わす ClassLoader 派生型のインスタンスがヒープ中からなくなること。

クラスが 1. 〜 3. の条件を満たしているかどうかの判断は、実装的な理由により GC 時に行われる Java VM が多い。

クラスのアンロードを積極的に利用した場合、3. が問題となる。
下のプログラムを用いて説明すると、myLoader クラスローダーの loadClass メソッドを呼び出した jp.nminoru.Hoge クラスが myLoader の中に入っているという保証はない。 myLoader の親クラスローダーが jp.nminoru.Hoge クラスを解決してロードしてしまう可能性があるからだ。 その場合には、myLoader を破棄しても jp.nminoru.Hoge がアンロードされない。

MyClassLoader myLoader = new MyClassLoader();

Class aClass = myLoader.loadClass("jp.nminoru.Hoge");

myLoader = null;
aClass   = null;

System.gc(); 

またブーストラップクラスローダーは Java VM が存在する限り消滅することはない。 そのためブーストラップクラスローダーによって読み込まれたクラスは決してアンロードされない。 システムクラスローダーも通常のライブラリの実装では、アンロードされないと考えないといけない。

2. クラスアンロードの方法

プログラムの実行中に、一部のクラスをロード & アンロードするプログラムを作成することを考えよう。

まずプログラムの設計として、アンロードしないクラスとアンロードの対象となるクラスを分ける必要がある。
アンロードしないクラスはシステムクラスローダーで読み込むことにする。 基本的に java.* などのシステム定義のクラスが格納された $(JRE)/lib/rt.jar JAR ファイルと、Java VM 起動時に -classpath で定義されたパス上にあるクラスファイル・JAR ファイルを検索の対象とする。 アンロードしないクラスはこの -classpath の上に配置することになる。

一方、アンロードするクラスはシステムクラスローダーに読み込まれない位置に保存し、アンロード可能なクラスローダーから読み込むことにする。 クラスのアンロード可能なクラスローダーを作る場合には、java.net.URLClassLoader から派生を行うのが便利である。
URLClassLoader では Java VM の -classpath で指定されないパスを 検索パスとして取ることができる。
ローカルディスクのディレクトリを検索パスに含める場合には "file:/directory1/" ("/" で終わること)、ローカルディスク上の JAR ファイルを検索パスに含める場合には "jar:/directory2/file.jar!/" を指定する。

import java.net.URL;
import java.net.URLClassLoader;

URLClassLoader loader = new URLClassLoader( new URL[] { 
                                              new URL("file:/directory1/"),
					      new URL("jar:/directory2/file.jar!/"),
					      } );

// クラスの読み込み
loader.loadClass( ...);

// アンロード
loader = null;
System.gc();

アンロード可能なクラスを配置するクラスパスを Java VM の -classpath 引数と同じ形で与えたい場合、以下の convertClasspathToURLs のような変換メソッドを利用する(このプログラムをコンパイルするには J2SE v1.4 以上必要)。
システムのパス区切り文字などの認識した上で URL 配列に変換してくれる。

import java.io.File;
import java.io.InputStream;
import java.io.IOException; 
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.regex.PatternSyntaxException;
import java.util.Vector;

public static URL[] convertClasspathToURLs(String classpath) {
  Vector tmpArray = new Vector();
  URL[] urls = null;

  try {
    String[] parts = classpath.split(File.pathSeparator);
            
    for (int i=0 ; i<parts.length ; i++) {
      final String path = parts[i];

      try {
        URL url = null;
        final String postfix = path.substring(path.length() - 4, path.length());
        if (postfix.equalsIgnoreCase(".jar") || postfix.equalsIgnoreCase(".zip")) {
          final String base = (new File(path).getCanonicalFile().toURL()).toString();
          url = new URL("jar:" + base + "!/");
        } else {
          url = new File(path).getCanonicalFile().toURL();
        }

        tmpArray.add(url);
      } catch(IOException e) {
        // through
      }
    }
  } catch(PatternSyntaxException e) {
    throw new IllegalArgumentException();
  } catch(NullPointerException e) {
    throw new IllegalArgumentException();
  }

  urls = new URL[tmpArray.size()]; 
	
  for(int j=0 ; j<tmpArray.size() ; j++) {
    urls[j] = (URL) tmpArray.get(j);
  }

  return urls;
}

3. サンプルプログラム

UnloadableClassLoader.java

クラスアンロード可能なクラスローダーを使う例です。

まず、main メソッドを実装した実行可能な SampleProgram クラスを作り、 これを適当なディレクトリ (ex: /home/nminoru/program/) におきます。 これをディレクトリを移動せずに java で実行するには以下のようになります。
/home/nminoru/program/ を CLASSPATH 環境に含めないでください。

java -cp /home/nminoru/program/ SampleProgram arg1 arg2 ...

上の方法では SampleProgram はブートストラップクラスローダーによってロードされ 実行されます。

次に、 UnloadableClassLoader.java をコンパイルした UnloadableClassLoader.class を CLASSPATH 環境のパスが通った位置に起き、 以下のコマンドを実行します。

java UnloadableClassLoader /home/nminoru/program/ SampleProgram arg1 arg2 ...

この例では、 SampleProgram はアンロード可能なクラスローダーに読み込まれた後に 実行されるようになります。

AppLoader.java

クラスローダーの委譲関係を破壊して、 システムクラスローダーを乗っ取ってしまう例。

  1. AppLoader は URLClassLoader を継承し、 クラスパス (classpath) を自身の検索パスとする。 ただしクラスパス中にディレクトリの指定があった場合、 そのディレクトリの直下にある JAR ファイルも自動的に検索パスに含める。
  2. AppLoader は loadClass メソッドをオーバーライドすることで、 システムクラスローダーにクラス解決の委譲する代わりに、 システムクラスローダーの親クラスローダーにクラスの解決を委譲する。 つまりシステムクラスローダーがスキップされる。
  3. システムクラスローダーの親クラスローダーの loadClass(String,boolean) は、 protected メソッドなためそのままでは呼び出すことができない。 リフレクションを用いて強制的に呼び出しをかける。

使い方は、以下のように複数の JAR ファイルをクラスパスに指定するプログラムの場合、

java -cp .:./lib/A.jar:./lib/B.jar:./lib/C.jar SampleProgram arg1 arg2 ...

AppLoader を噛ませると ../lib/ 以下にある JAR ファイルを自動的に検索パスに追加してくれる。

java -cp .:./lib/ AppLoader SampleProgram arg1 arg2 ...

JAR ファイルの展開は禁止できないので、 クラスパスで指定したディレクトリには関係のない JAR ファイルは置かないようにする必要がある。

4. おまけ

クラスアンロードの監視方法

あるクラスがシステム中からアンロードされたかどうかを調べるには、 調べたいクラスに対応する java.lang.Class のインスタンスがガーベージコレクションによって回収されたかどうかチェックすればよい。 Class のインスタンスを弱参照でチェックする。

import java.lang.ref.WeakReference;

Class targetClass = ...
WeakReference targetClassWR = new WeakReference(targetClass);

// ...
 
if (targetClassWR.get() != null) {
  // targetClass が指すクラスはまだ読み込まれている。
} else {
  // targetClass が指すクラスはすでにアンロードされている。
}

ただし、この方法が Java の言語仕様上許されているかどうかはグレイである。 Java 言語仕様書の第3版には、Class オブジェクトは、クラスがロードされる際に Java 仮想マシンによって、およびクラスローダの defineClass メソッドの呼び出しによって自動的に構築されます。 とあるが、クラスがアンロードされたときに Class オブジェクトが破棄されるという保証はない。 だが Sun・IBM・BEA の JavaVM では期待通りの動作をしている。


コメント

コメントを書き込む
[1] [名無しさん] 2004-08-26 23:44:00
実は偶然通りがかったのですが、、、、、、
とても参考になります。今やってる仕事にも役に立ちそうです。
ありがとうございます。
[2] [名無しさん] 2006-12-27 09:23:01
非常に興味深く読ませていただきました。
ありがとうございます!
[3] [Richard Stallman] 2008-05-28 09:43:28
Java バンザイ!
[4] [のり] 2010-03-19 13:30:26
クラスローダーについて勉強になりました。
[5] [名無しさん] 2012-04-13 11:22:40
非常にわかりやすく、理解できました。ありがとうございました!
[6] [lulu] 2021-03-10 14:39:49
大変参考になりました。ありがとうございます。
一点だけ、

「オブジェクト(java.lang.Object) がクラス(java.lang.Class) に所属するように」

ObjectがClassに属しているのでしょうか?私が知っている限りでは、ClassはJVMがクラスをロードするときにそのクラスのClassオブジェクトを生成している役割ですが・・

[7] [管理人] 2021-03-16 00:34:05
Javaの文法上のクラスはjava.lang.Classのオブジェクトと1対1に対応しているので、私は「文法上のクラス=java.lang.Classのインスタンス」と考えています。そう考えると異なるクラスローダ―上にある同名のクラスの扱いがうまく整理できるからです。

初期のJDK 1.0から7前後までJava VMのソースコードを読んでいましたが、java.lang.Object はその第1フィールドに「private Class _class」のようなフィールドを概念的に持っています。これは他の JVM でも同様だと思います。そしてオブジェクト(java.lang.Object)が生成される時に、_classフィールドに Class オブジェクトの参照が fill されるように動作します。
# Hotspot VM の実際の実装は _class に相当するフィールドはもっと複雑な構造をしていますが。

java.lang.Object → java.lang.Class → java.lang.ClassLoader という参照関係があり、オブジェクト指向で言うところの「A has B」という関係があるの「オブジェクトはクラスに所属し、クラスはクラスローダ―に所属する」と言えると考えています。


こう書くと java.lang.Object を継承しているはずの java.lang.Class や java.lang.ClassLoader は、そのオブジェクト生成時に Class も ClassLoader もないのにどうやって生成できるのか疑問に思われると思いますが、クラスファイルを読み込んで作られるのではなく JVM 起動時に自動生成されます。そういう点も踏まえて「Bootstrap Class Loader」という特別なクラスローダ―が用意されているわけです。

TOP    掲示板    戻る
Written by NAKAMURA Minoru, Email: nminoru atmark nminoru dot jp, Twitter:@nminoru_jp