Internals of Java Class LoadingJavaクラス・ローディングの内部処理

Binildas Christudas
2005年1月26日


クラス・ローディングは、Java言語仕様が提供するメカニズムの中で、もっとも強力なものの1つに数えられます。 クラス・ローディングの内部処理が"上級トピック"に分類されるとは言え、このメカニズムがどのように機能しており、要件に合わせて何を実行できるかは、すべてのJavaプログラマが知っておくべき内容です。 これらを理解することで、ClassNotFoundExceptionClassCastExceptionなどのデバッグに費やされていた時間を節約できるようになります。

この記事では、コードとデータの違いや、どのようにしてこれらがインスタンスやオブジェクトの形成に関連するか、といった基本的な事項から説明を始めます。 次に、クラス・ローダーを使用してJVMへコードをロードするメカニズムについて、またJavaで使用できるおもなクラス・ローダーの種類について確認します。 さらに、基本的なアルゴリズム(プロービング)を使用して、クラス・ローダーの内部処理について検証した後で、クラスをロードする前のクラス・ローダーについて確認します。 次のセクションではコード例を使用して、開発者が独自のクラス・ローダーを拡張および開発する必要性を示します。 その後で、独自のクラス・ローダーを作成し、これらを使用して汎用タスク実行エンジンを構築する方法について説明します。この実行エンジンを使用すると、任意のリモート・クライアントに提供されたコードをロードし、JVM内に定義し、インスタンス化して実行できます。 最後に、カスタムのクラス・ローディング・スキーマが標準となっているJ2EE固有のコンポーネントを参照して、この記事を締めくくります。

クラスとデータ

クラスが、実行するためのコードを表すのに対して、データは、コードに関連付けられた状態を表します。 状態は変化しますが、通常、コードは変化しません。 特定の状態をクラスに関連付けるときに、このクラスのインスタンスを作成します。 したがって、同じクラスに対する異なるインスタンスでは状態が異なる場合もありますが、すべて同じコードを参照しています。 Javaでは、例外はあっても、通常、クラスのコードは.classファイル内に格納されています。 しかし、Javaランタイムでは、すべてのクラスのコードが、ファーストクラスJavaオブジェクト、つまりjava.lang.Classのインスタンスの形式でも提供されます。 Javaファイルをコンパイルするたびに、コンパイラによって、classという名前のjava.lang.Class型のpublic static finalフィールドが、生成されたバイトコード内に埋め込まれます。 このフィールドはpublicであるため、次のようにドットを使用してアクセスできます。

java.lang.Class klass = Myclass.class;

いったんJVMにクラスがロードされたら、同じクラス(繰り返しますが、同じクラスです)が再びロードされることはありません。 ここで疑問になるのは、"同じクラス"とはいったい何を意味するのか、という点です。 オブジェクトが特定の状態やIDを保持し、オブジェクトは常にそのコード(クラス)に関連付けられているいう条件と同様に、JVMにロードされたクラスには特定のIDが指定されています。このIDについて、詳しく確認してみましょう。

Javaでは、クラスはその完全修飾クラス名によって識別されます。 完全修飾されたクラス名は、パッケージ名とクラス名で構成されています。 しかし、JVMでは、完全修飾クラス名に加えて、クラスをロードしたClassLoaderのインスタンスによってクラスが一意に識別されます。 このように、パッケージPgに含まれるクラスClが、クラス・ローダーKlassLoaderのインスタンスkl1によってロードされた場合、C1のクラス・インスタンス(C1.class)のJVMでのキーは(Cl, Pg, kl1)になります。 つまり、(Cl, Pg, kl1)と(Cl, Pg, kl2)という2つのクラス・ローダー・インスタンスは同一ではなく、これらによってロードされたクラスもまったく異なるものであり、互いの型に互換性はありません。 では、JVMにはいくつのクラス・ローダー・インスタンスが含まれるのでしょうか。 これについて、次のセクションで説明します。

クラス・ローダー

JVMでは、すべてのクラスがjava.lang.ClassLoaderのインスタンスによってロードされます。 ClassLoaderクラスはjava.langパッケージに含まれており、開発者はこのクラスのサブクラスを自由に作成し、独自のクラス・ローディング機能を追加することができます。

java MyMainClassというコードによってJVMが開始されるたびに、java.lang.Objectなどの主要なJavaクラスとその他のランタイム・コードが、"ブートストラップ・クラス・ローダー"によって最初にメモリにロードされます。 ランタイム・クラスはJRE\lib\rt.jarファイル内にパッケージ化されています。 ブートストラップ・クラス・ローダーはネイティブ実装であるため、Javaドキュメントに詳しい説明は記載されていません。 また、同じ理由から、JVMが異なるとブートストラップ・クラス・ローダーの動作も異なります。

関連する注意事項として、コアJavaのランタイム・クラスに対してクラス・ローダーを取得しようとすると、nullが返されます。次にその例を示します。

    log(java.lang.String.class.getClassLoader());

次は、Java拡張クラス・ローダーです。 コアJavaのランタイム・コードの枠を超えた機能を提供する拡張ライブラリを、java.ext.dirsプロパティに指定されたパスに保管できます。 ExtClassLoaderは、java.ext.dirsのパスに含まれるすべての.jarファイルをロードする責任を負います。 開発者が独自のアプリケーションの.jarファイルや、クラスパスに追加する必要のあるライブラリをこの拡張ライブラリに追加すると、これらは拡張クラス・ローダーによってロードされます。

3番目のクラス・ローダーであるAppClassLoaderは、開発者の観点から見てもっとも重要なクラス・ローダーです。 このアプリケーション・クラス・ローダーは、java.class.pathシステム・プロパティに指定されたパスに含まれるすべてのクラスをロードします。

"これら3つのクラス・ローダー・パスについて、詳しくはSunのJavaチュートリアル『Understanding Extension Class Loading』を参照してください。 次に、JDKに含まれる他のいくつかのクラス・ローダーの例を挙げます。

  • java.net.URLClassLoader
  • java.security.SecureClassLoader
  • java.rmi.server.RMIClassLoader
  • sun.applet.AppletClassLoader

java.lang.Threadに含まれるメソッドpublic ClassLoader getContextClassLoader()は、特定のスレッドに対するコンテキスト・クラス・ローダーを返します。 コンテキスト・クラス・ローダーはスレッドの作成者によって提供され、クラスやリソースのロード時にこのスレッドで実行されるコードで使用されます。 コンテキスト・クラス・ローダーが設定されていない場合、デフォルトは親スレッドのクラス・ローダー・コンテキストになります。 通常、最初のスレッドのコンテキスト・クラス・ローダーは、アプリケーションのロードに使用されたクラス・ローダーになります。

クラス・ローダーの仕組み

ブートストラップ・クラス・ローダーを除くすべてのクラス・ローダーには、親クラス・ローダーが存在します。 また、すべてのクラス・ローダーの型はjava.lang.ClassLoaderになります。 上述の2つの文は異なりますが、開発者が作成したクラス・ローダーが正しく動作するためには非常に重要です。 もっとも重要な要素は、親クラス・ローダーを正しく設定することです。 すべてのクラス・ローダーの親クラス・ローダーは、そのクラス・ローダーをロードしたクラス・ローダー・インスタンスです (クラス・ローダー自体もクラスであることに注意してください)。

クラス・ローダーはloadClass()メソッドを使用してクラスを要求します。 このメソッドの内部処理は、次に示すjava.lang.ClassLoaderのソース・コードから確認できます。

protected synchronized Class<?> loadClass
    (String name, boolean resolve)
    throws ClassNotFoundException{

    // First check if the class is already 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 to find the class.
            c = findClass(name);
        }
    }
    if (resolve) {
	    resolveClass(c);
    }
    return c;
}

ClassLoaderコンストラクタ内で親クラス・ローダーを設定する方法には、次の2つの方法があります。

public class MyClassLoader extends ClassLoader{

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

または

public class MyClassLoader extends ClassLoader{

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

コンストラクタ内からのgetClass()メソッドの呼出しは避けるべきであるため、推奨されるのは1番目の方法です。これは、オブジェクトの初期化がコンストラクタ・コードの終了時にはじめて完了するためです。 したがって、親クラス・ローダーが正しく設定されている限り、ClassLoaderインスタンスからクラスが要求された場合にクラスが見つからなければ、最初に親に確認する必要があります。 親からもクラスが確認できず(親クラス・ローダーからもクラスが見つからないなど)、またfindBootstrapClass0()メソッドも失敗する場合、findClass()メソッドが起動されます。 findClass()のデフォルト実装ではClassNotFoundExceptionが投げられます。また、開発者がjava.lang.ClassLoaderのサブクラスを作成してカスタム・クラス・ローダーを作成する際は、このメソッドの実装が求められます。 findClass()のデフォルト実装は次のようになっています。

    protected Class<?> findClass(String name)
        throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

findClass()メソッドの内部では、クラス・ローダーが任意のソースからバイトコードをフェッチする必要があります。 ソースには、ファイル・システムやネットワークURL、データベース、または処理中にバイトコードを生成するその他のアプリケーションなどがあります。また、Javaバイトコード仕様に準拠したバイトコードを生成できる類似ソースも対象になります。 その他に、BCEL(Byte Code Engineering Library)を使用することもできます。BCELは、実行時にゼロからクラスを作成するための便利なメソッドを提供します。 BCELは、コンパイラやオプティマイザ、オブファスケータ、コード・ジェネレータ、分析ツールなどのさまざまなプロジェクトで問題なく使用されています。 バイトコードが取得されたら、このメソッドからdefineClass()メソッドを呼び出す必要があります。すると、ランタイムは、このメソッドを呼び出したClassLoaderインスタンスに特化したものになります。 したがって、2つのClassLoaderインスタンスによってバイトコードが定義された場合、同じソースから取得していても、別のソースから取得していても、定義されたクラスは異なるクラスになります。

Java実行エンジンでのインタフェースとクラスのロードリンク初期化プロセスについて、詳しくは『Java Language Specification』を参照してください。

図1に、MyMainClassという名前のメイン・クラスを使用したアプリケーションを示します。 前述のとおり、MyMainClass.classはAppClassLoaderによってロードされます。 MyMainClassは、CustomClassLoader1とCustomClassLoader2という2つのクラス・ローダーのインスタンスを作成します。これらのインスタンスは、何らかのソース(たとえばネットワーク・パス)から、Targetと呼ばれる4番目のクラスのバイトコードを見つけることができます。 つまり、Targetクラスのクラス定義はアプリケーション・クラスパスや拡張クラスパスには含まれていません。 このようなシナリオで、MyMainClassからカスタム・クラス・ローダーにTargetクラスをロードするように要求が出されると、CustomClassLoader1とCustomClassLoader2はそれぞれ別々にTargetをロードし、Target.classを定義します。 これは、Javaにおいて深刻な結果を招きます。 Targetクラスに静的な初期化コードが含まれており、このコードをJVM内で1回だけ実行したい場合にも、現在の設定では、2つのCustomClassLoaderによって別々にクラスがロードされるたびに1回ずつ、合計2回、このコードがJVM内で実行されます。 図1に示すとおり、2つのCustomClassLoader内でTargetクラスがそれぞれインスタンス化され、target1インスタンスとtarget2インスタンスが作成された場合、target1とtarget2の型に互換性はありません。 言い換えると、JVMは次のコードを実行できません。

    Target target3 = (Target) target2;

上記コードを実行すると、ClassCastExceptionが投げられます。 これらのインスタンスは異なるClassLoaderインスタンスによって定義されているため、JVMからは異なる別々のクラス型と見なされるためです。 また、CustomClassLoader1とCustomClassLoader2のように2つの独立したクラス・ローダーが使用されるのではなく、同じCustomClassLoaderクラスに対する2つの異なるインスタンスがMyMainClassで使用された場合も、上述の説明が当てはまります。 これについては、この記事の後半でコード例を使用して説明します。


図1. 1つのJVM内で、同じTargetクラスをロードする複数のClassLoader

クラスのロード、定義、リンクといったプロセスについて、詳しくはAndreas Schaeferの記事『Inside Class Loaders』を参照してください。

▲ ページTOPに戻る