Java ClassLoader

ClassLoader とは

JVM はクラスをロードするために Class Loader と呼ばれるものを使用している。 Class Loader は 1 つだけではなく、JVM が用途別に複数のクラスローダーを使うほか、J2EE コンテナでは複数のクラスローダーが階層的に追加定義されており、ユーザーが独自に定義することも可能である。

Class Loader はクラスをメモリ上にロードする。言い換えると、コンパイルされたクラス(.class)を動的にメモリへロードする役割を持つ。ここで動的にロードするとは、コンパイル時ではなく実行時にクラスファイルが必要になって実行する場合に、メモリへ載せるという意味である。

ClassLoader の基本動作

J2SE に定義されたクラスローダーは次の 3 つである。

  • ブートストラップクラスローダー(Bootstrap Class Loader)
    • 初期クラスローダー
    • コアパッケージなど(rt.jar、i18n.jar)のクラスローダー
  • 拡張クラスローダー(Extension Class Loader)
    • 拡張機能のインストール型オプションパッケージ(lib/ext)のクラスローダー

    • Java 8 まで: URLClassLoader を継承し、jre/lib/ext および java.ext.dirs 環境変数で指定されたフォルダ内のクラスをロードする。

    • Java 9 以降: jre/lib/ext と java.ext.dirs をサポートせず、Java SE のすべてのクラスと JCP(Java Community Process)によって標準化されたモジュールのクラスをロードする。

      • BuiltinClassLoader を継承して ClassLoader の内部 static クラスとして実装され、PlatformClassLoader に変更された。
  • ユーザー定義クラスローダー(System Class Loader、Application Class Loader)
    • classpath クラスローダー

クラスローダー間には親子関係があり、親クラスローダーは子クラスローダーを知らないが、子クラスローダーは親を知っている。子クラスローダーにクラスロード要求が入ると、親に委譲して最初に見つかったクラスをロードする。もし対象クラスが、要求を受けたクラスローダーより子クラスローダーの責任範囲にあるなら、そのクラスは見つからずロードに失敗する。

  1. クラスローダーがクラスロードを要求される。loadClass()
  2. すでにロードされているか確認する。findLoadedClass
  3. ロードされていなければ、親クラスローダー(ない場合はブートストラップクラスローダー findBootstrapClassOrNull())へ処理を委譲(delegation)する。parent.loadClass(name, false)
  4. ロードされなければ探してロードを試みる。findClass()

具体的には java.lang.ClassLoaderloadClass(..) メソッドを見ると、上記の内容を確認できる。

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

ブートストラップクラスローダーはコアパッケージなどをロードするクラスローダーであり、それ自体は親を持たない。ユーザー定義クラスローダーは、起動時に実行されるシステムクラスローダーを親として派生する。

たとえば J2EE コンテナの場合、EAR のユーティリティ JAR をロードするクラスローダーは、WAR の Web コンポーネントをロードするクラスローダーの親になるため、Web コンポーネントは EAR のユーティリティ JAR を呼び出せる。逆に、EAR のユーティリティ JAR は子である WAR 内の Web コンポーネントを呼び出せない。

また、static で限定されたフィールドはクラスロード時に一度だけ実行されるため、クラスローダー内で 1 つになる。クラスローダーを統合すればユーティリティクラスを共通クラスにできるが、予期しない副作用が発生する可能性があるため、動作を理解してよく確認する必要がある。

クラスローダーは抽象クラス java.lang.ClassLoader を継承したクラスである。たとえば、コアパッケージ内では java.security.SecureClassLoaderjava.net.URLClassLoader がこのクラスを継承したクラスとして定義されている。

ClassLoader を検索する例

次のコードは、クラスローダーを検索する例である。

package com.devkuma.basic.classloader;

class GetClassLoader {
    public static void main(String[] args) {
        // Bootstrap class loader
        System.out.println("- Bootstrap Class Loader");
        ClassLoader bootstrap = "".getClass().getClassLoader();
        System.out.println(bootstrap);
        if (bootstrap != null) {
            ClassLoader parent = bootstrap.getParent();
            System.out.println(parent);
        } else {
            System.out.println("No parent");
        }

        // Platform Class Loader
        System.out.println("- Platform Class Loader");
        ClassLoader extensions = GetClassLoader.class.getClassLoader().getParent();
        System.out.println(extensions);
        if (extensions != null) {
            ClassLoader parent = extensions.getParent();
            System.out.println(parent);
        } else {
            System.out.println("No parent");
        }

        // System Class Loader
        System.out.println("- System Class Loader");
        DemoClass myObj = new DemoClass();
        ClassLoader systems = myObj.getClass().getClassLoader();
        System.out.println(systems);
        if (systems != null) {
            ClassLoader parent = systems.getParent();
            System.out.println(parent);
        } else {
            System.out.println("No parent");
        }

        // Context Classloader
        System.out.println("- Context Classloader");
        ClassLoader threads = Thread.currentThread().getContextClassLoader();
        System.out.println(threads);
        if (threads != null) {
            ClassLoader parent = threads.getParent();
            System.out.println(parent);
        } else {
            System.out.println("No parent");
        }
    }
}

class DemoClass {
    String getClassName() {
        return "DemoClass";
    }
}

次のように出力される。ここでコンテキストクラスローダーは、そのスレッド内でクラスをロードするために使用されるクラスローダーである。

- Bootstrap Class Loader
null
No parent
- Platform Class Loader
jdk.internal.loader.ClassLoaders$PlatformClassLoader@7dc5e7b4
null
- System Class Loader
jdk.internal.loader.ClassLoaders$AppClassLoader@799f7e29
jdk.internal.loader.ClassLoaders$PlatformClassLoader@7dc5e7b4
- Context Classloader
jdk.internal.loader.ClassLoaders$AppClassLoader@799f7e29
jdk.internal.loader.ClassLoaders$PlatformClassLoader@7dc5e7b4