Javaのクラスローダ

Ideas - O'Reilly Media
訳すとかではなくて、これを読んで知識を整理してみた。

どのクラスにもpublic static final でclassというjava.lang.Class型のフィールドが出来る(MyClass.classみたいにアクセスする)。ただしjava.lang.Objectに定義されているわけではない。
クラスのアイデンティティは「クラス名」「パッケージ」「そのクラスをロードしたクラスローダ」の3つで決まる。つまり異なるクラスローダが「同じパッケージの同じクラスを同じclassファイルから」ロードした場合、それぞれが異なるクラスと認識される(キャストするとClassCastExceptionが起こる)。
普通のjavaプログラムを実行したときに使われるクラスローダは以下の3つ

  • ブートストラップクラスローダ:JRE/lib以下の基本ライブラリを読む(java.langとか)。native実装。getClassLoader()してブートストラップクラスローダならnullが返る。
  • ExtClassLoader:JRE/lib/ext以下(システムプロパティ"java.ext.dirs"で決まる)の拡張ライブラリを読む。
  • AppClassLoader:クラスパスに通っているクラスを読む。システムクラスローダと呼ばれたりもする

クラスローダの親子関係はこのようになる(クラスの継承関係ではない)

ブートストラップクラスローダ
 |
ExtClassLoader
 |
AppClassLoader


クラスの継承関係はこのようになる。

ClassLoader
 |
SecureClassLoader
 |
URLClassLoader
 |        
AppClassLoader(ExtClassLoaderも同じ)

クラスローダはjava.lang.ClassLoaderを継承する必要がある。URLで指定するリソースからロードするならば、とりあえずURLClassLoaderを継承すればよさそう(JBossのUnifiedClassLoaderもそうしている)。
クラスローダは親クラスローダを持つ(ブートストラップクラスローダ以外)。デフォルトではクラスローダ(のクラス自体)をロードしたクラスローダが親になる。親クラスローダを明示的に指定する場合、クラスローダを引数にするsuperのコンストラクタを呼ぶ。

public MyClassLoader() {
  super(MyClassLoader.class.getClassLoader);
}

クラスローダはloadClass()時に、自分がそのクラスを知らなければ親に委譲する。
ブートストラップクラスローダまで行き着いても誰も知らなければ、ブートストラップクラスローダが自分の守備範囲(基本ライブラリ)からクラスをfindClass()する。見つかれば返し、見つからなければClassNotFoundExceptionを投げる。親からClassNotFoundExceptionが飛んできたらcatchし、自分で自分の守備範囲からfindClass()する。この繰り返しで最後まで見つからなければアプリにClassNotFoundExceptionがきて終了。
ただしこれはjava.lang.ClassLoaderのloadClass()をベースにした流れなので、自分でオーバーライドしたら全く違う動きに出来る。

ClassLoaderのloadClass():

protected synchronized Class loadClass(String name, boolean resolve)
throws ClassNotFoundException
  {
  // First, check if the class has already been loaded
  Class c = findLoadedClass(name);
  if (c == null) {
    try {
    if (parent != null) {
      c = parent.loadClass(name, false);
    } else {
      c = findBootstrapClass0(name);
    }
    } catch (ClassNotFoundException e) {
    // If still not found, then invoke findClass in order
    // to find the class.
    c = findClass(name);
    }
  }
  if (resolve) {
    resolveClass(c);
  }
  return c;
}

java.lang.ClassLoaderではfindClass()はClassNotFoundExceptionを投げるだけ。実装クラス(たとえばURLClassLoader)ではfindClass()でリソースを見つけ、それをもとにdefineClass()する。
CL1がMyClassをロードするためloadClass()を呼び出し、委譲されたCL2がクラスをfind、defineした場合、MyClassのクラスローダ(getClassLoader()の結果)は当然ながらCL2のほうである。nativeのdefineClass0()あたりでセットされるのだろうか。

「Internals of Java Class Loading」ではこのあとカスタムクラスローダのサンプルになるが、要は2つのクラスローダインスタンスを使ったから、同じクラス名でも2種類の実装をロードできた、という話。

さらに調べること:

  • Class.forName()でなく、実際にクラスが実行されるときのクラスロード。たぶんresolveClass()や、Threadのコンテキストクラスローダについて
  • アプリケーションサーバのクラスローダ。TomcatJBossWebLogicとか