🍍

8.1 クラスローダーの仕組みとClassクラス(クラスのライフサイクル、ClassLoaderなど)~Java Advanced編

2023/11/05に公開

はじめに

自己紹介

皆さん、こんにちは、斉藤賢哉と申します。私はこれまで、25年以上に渡って企業システムの開発に携わってきました。特にアーキテクトとして、ミッションクリティカルなシステムの技術設計や、Javaフレームワーク開発などの豊富な経験を有しています。
様々なセミナーでの登壇や雑誌への技術記事寄稿の実績があり、また以下のような書籍も執筆しています。

いずれもJava EEJakarta EE)を中心にした企業システム開発のための書籍です。中でも 「アプリケーションアーキテクチャ設計パターン」は、(Javaに限定されない)比較的普遍的なテーマを扱っており、内容的にはまだまだ陳腐化していないため、興味のある方は是非手に取っていただけると幸いです(中級者向け)。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『Java Advanced編』の一部の範囲をカバーしたものです。『Java Advanced編』はこちらのリンクから購入できます(セールス対象外のためいつも同じ価格)。また定価の約30%OFFで購入可能なクーポンをZenn内で定期的に発行していますので、興味のある方は、ぜひ私の他の記事をチェックしてみてください。

この講座は、以下のような皆様にお薦めします。

  • Javaの基本的なスキルを習得済みで、さらなるレベルアップを目指している方
  • 将来的なキャリアとして、希少性の高い上級エンジニアやアーキテクトを志向している方
  • フリーランスエンジニアとして付加価値の更なる向上を図っている方
  • 「Oracle認定Javaプログラマ」の資格取得を目指している方

この記事を含むシリーズ全体像

この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。

https://zenn.dev/kenya_saitoh/articles/3fe26f51ab001b

8.1 クラスローダーの仕組みとClassクラス

チャプターの概要

このチャプターでは、クラスをメモリにロードする仕組みであるクラスローダーや、クラスのメタ情報を管理するClassクラスについて学びます。

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

クラスのライフサイクルとクラスローダーの役割

クラスのライフサイクルについては『Java Basic編』のチャプター8.1でも取り上げていますが、復習も兼ねて改めて説明します。
あるクラスが実行されると、Javaランタイムは、実行対象のクラスや、その中からアクセスされるその他のクラスのクラスファイル(.classファイル)をファイルシステムから検索し、メモリにロードします。このようにクラスを検索し、メモリにロードする仕組みをクラスローダーと呼びます。クラスローダーがクラスファイルを検索するときに起点となるパスは、モジュールシステムが導入されている場合はモジュールパス、そうでない場合はクラスパスになります。
クラスローダーによって読み込まれたクラス情報(一種のDNA設計情報)は、「スタティック領域」というメモリ空間にロードされます。ひとたびクラス情報がメモリ空間にロードされると、基本的には、再度同じクラス情報が読み込まれることはありません。

【図8-1-1】クラスのライフサイクル
image.png

この時点でスタティックなメンバーには、すでにアクセス可能になっています。
次にこのクラスのインスタンスが生成されると、生成されたインスタンスは「ヒープ領域」というメモリ空間に格納されます。1つのクラスからインスタンスはいくつでも生成することができますが、都度、生成されたインスタンスはヒープ領域に格納されていきます。インスタンスメンバーには、この時点で初めてアクセス可能になります。
ヒープ領域上のインスタンスは、「もう使われない」と見なされるとマークが付き、特定のタイミングでJavaランタイムによって自動的に解放されます。このようなJavaランタイムの仕組みを、ガベージコレクションと呼びます。

クラスローダの種類と階層構造

Javaランタイムには、いくつかの組み込みクラスローダーがあり、以下のように階層構造になっています。

  1. ブートストラップ・クラスローダー
  2. プラットフォーム・クラスローダー
  3. システム・クラスローダー

まず最上位のブートストラップ・クラスローダーは、java.lang.Stringクラスなど、Java SEのクラスライブラリによって提供される主要なクラスをロードするためのクラスローダーです。
次にプラットフォーム・クラスローダーですが、これも主にJava SEクラスライブラリをロードするクラスローダーで、例えばjava.net.http.HttpClientクラスなどをロードします。
通常この両者の違いは、あまり意識する必要はないでしょう。
最後にシステム・クラスローダーとは、アプリケーション・クラスローダーとも呼ばれ、開発者が作成したクラスをロードするためのクラスローダーです。
クラスローダーは、クラスをロードするとき、まず自分の親に対してロードを委譲し、対象クラスがなかった場合は自分がロードを担当する、という仕組みになっています。

【図8-1-2】クラスローダの種類と階層構造
Image.png

例えば開発者が「java.lang.String」という名前で「偽のStringクラス」を作ってロードしようと試みるケースを考えます。クラスのロードは、システム・クラスローダー→プラットフォーム・クラスローダー→ブートストラップ・クラスローダーという順に処理が委譲されます。そして最上位のブートストラップ・クラスローダーにおいて、Java SEのクラスライブラリの「正式なStringクラス」がロードされるため、開発者が「java.lang.String」という偽クラスを作っても、それがロードされることはありません。
なおクラスローダーの仕組みでは、組み込みのもの以外に、独自に作成したものを階層構造に追加していくことができます。例えばJakarta EE (旧Java EE) のアプリケーションサーバーには、アプリケーションを表すWARファイルと呼ばれるアーカイブファイルごとに、それを読み込むためのシステム・クラスローダが用意されています。このようにすることで、様々なアプリケーションを隔離しながら、1つのJavaランタイム上で動作させることを実現しています。

クラスがロードされるタイミング

クラスに対して以下のような処理が行われると、クラスローダによってクラスがロードされます。

(1)スタティックなメンバーへのアクセス
(2)ClassクラスのforName()メソッド呼び出し
(3)new演算子によるインスタンス生成
(4)ConstructorクラスのnewInstance()メソッド呼び出し

このうち(1)と(3)は馴染みのある方法ですが、(2)と(4)はリフレクションという機能によるもののため、次のチャプターで取り上げます。
さて前述したようにクラスのライフサイクルでは、「クラスのロード」と「インスタンス生成」は異なるフェーズですが、(1)~(4)をこれらのフェーズに当てはめると、まず(1)および(2)では「クラスのロード」のみが行われます。(3)および(4)では、メモリ(スタティック領域)上にすでにクラス情報が読み込まれている場合は、それをもとに「インスタンス生成」のみが行われます。まだメモリ(スタティック領域)上にクラス情報が読み込まれていない場合は、「クラスのロード」→「インスタンス生成」という順に処理が行われます。

【図8-1-3】クラスがロードされるタイミング
image.png

ClassLoaderクラスのAPI

クラスローダーは、java.lang.ClassLoaderクラスによって表されます。
このクラスには数多くのAPIがありますが、本レッスンではそれらの中から代表的なものを取り上げます。

API(メソッド) 説明
static ClassLoader getPlatformClassLoader() プラットフォーム・クラスローダーを返す。
static ClassLoader getSystemClassLoader() システム・クラスローダーを返す。
ClassLoader getParent() 委譲のための親クラスローダーを返す。
InputStream getResourceAsStream(String name) 指定されたパスからリソースを読み込むためのInputStreamを返す。

以下のコードを見てください。

snippet (pro.kensait.java.advanced.lsn_8_1_1.Main_1)
ClassLoader classLoader1 = ClassLoader.getPlatformClassLoader(); //【1】
ClassLoader classLoader2 = ClassLoader.getSystemClassLoader(); //【2】
ClassLoader parent1 = classLoader1.getParent(); //【3】
ClassLoader parent2 = classLoader2.getParent(); //【4】

ClassLoaderクラスのgetPlatformClassLoader()メソッドを呼び出すとプラットフォーム・クラスローダーを、getSystemClassLoader()メソッドを呼び出すとシステム・クラスローダーを、それぞれ取得することができます【1、2】。
また、getParent()メソッドによって親となるクラスローダーを取得できます。プラットフォーム・クラスローダーの親はブートストラップ・クラスローダーですが、これはnull値として返されます【3】。システム・クラスローダーの親は、プラットフォーム・クラスローダーになります【4】。

リソースの読み込みとコンテキスト・クラスローダー

前述したようにクラスのロードはクラスローダーによって自動的に行われますが、開発者がクラスローダを明示的に利用するケースがあります。それがリソースの読み込みです。リソースとはクラス以外の静的コンテンツのことで、その代表がプロパティファイルなどの定義ファイルです。
プロパティファイルに限っては、リソースバンドルという仕組みによって、クラスローダーを使用することなくクラスパスから読み込むことが可能ですが、プロパティファイル以外の形式(例えばXMLやYAML)の定義ファイルをクラスパスから読み込むためには、プログラムの中でクラスローダーを取得する必要があります。
リソースは開発者が作成するものなので、それを読み込むためのクラスローダーは、必然的にシステム・クラスローダーになります。ただし前述したようにJakarta EEなどの環境では、アプリケーションごとに独自のシステム・クラスローダーが用意されているため、それを取得しなければなりません。このような場合はコンテキスト・クラスローダーを使用します。アプリケーションの実行環境を問わずに、開発者自身が作成したリソースをクラスパスから読み込むためには、コンテキスト・クラスローダーを使用するのが良いでしょう。
以下にコンテキスト・クラスローダーによって、クラスパス上のプロパティファイルを読み込むコードを示します。

snippet (pro.kensait.java.advanced.lsn_8_1_1.Main_2)
Properties props = new Properties();
ClassLoader classloader = Thread.currentThread().getContextClassLoader(); //【1】
try (InputStream is = classloader
        .getResourceAsStream("config/MyResource.properties")) { //【2】
    props.load(new InputStreamReader(is, StandardCharsets.UTF_8)); //【3】
    String name = props.getProperty("name");
    int age = Integer.parseInt(props.getProperty("age"));
    String address = props.getProperty("address");
    System.out.println(name + ", " + age + ", " + address);
} catch (IOException ioe) {
    throw new RuntimeException(ioe);
}

このコードの前提として、読み込み対象のプロパティファイル"MyResource.properties"は、クラスパスを起点としたconfigディレクトリに配置されているものとします。
このプロパティファイルをクラスパスから読み込むためには、コンテキスト・クラスローダーが必要です。コンテキスト・クラスローダーは、ThreadクラスのgetContextClassLoader()メソッドを呼び出すことで取得します【1】。
続いて取得したコンテキスト・クラスローダーのgetResourceAsStream()メソッドに、読み込み対象のプロパティファイルのパスを指定し、InputStreamを取得します【2】。ここで指定するパスは、クラスパス上のパスで、ディレクトリの区切り文字はOSに関わらず"/"を指定します。
次に取得したInputStreamと文字コード(UTF-8)からInputStreamReaderのオブジェクトを生成し、それをPropertiesクラスのload()メソッドに渡すことで、このプロパティファイルを読み込みます【3】。

Java SEであってもJakarta EEであっても、アプリケーションの実行環境を問わずにリソースを読み込みたい、また、読み込みたいリソースはプロパティファイル形式だけではなくXMLやYAMLなど様々な形式がある、といったケースでは、コンテキスト・クラスローダーを利用して、リソースを読み込むようにしてください。

8.1.2 Classクラスの特徴とAPI

Classクラスの特徴とリテラル

java.lang.Classクラスは、クラスやインタフェースのメタ情報を管理するためのクラスです。Classクラスが持つメタ情報には、そのクラス・インタフェースの名前や、ロードされたクラスローダー、メンバーとしてどんなフィールド、メソッド、コンストラクタを持っているか、といった情報が含まれます。
Classクラスのオブジェクト(以降Classオブジェクト)は、Javaランタイム内に1つしか存在せず、自動的に管理されるため、開発者が明示的に生成することはできません。
Classオブジェクトを表すためには、以下のようなリテラルを使います。

【構文】Classオブジェクトのリテラル
クラス名.class

例えばGreetingクラスのClassオブジェクトであれば、リテラルを利用して以下のように取得します。

snippet_1 (pro.kensait.java.advanced.lsn_8_1_2.Main)
Class<Greeting> clazz = Greeting.class;

Classクラスはジェネリックタイプのため、型パラメータとして対象のクラス名を指定します。

またすべてのクラスの親に当たるjava.lang.Objectクラスには、getClass()メソッドが定義されており、自身のClassオブジェクトを取得することができます。生成されたインスタンスからClassオブジェクトを取得したい場合は、このメソッドを使用します。
以下にその例を示します。

snippet_2 (pro.kensait.java.advanced.lsn_8_1_2.Main)
Greeting greeting = new Greeting();
Class<?> clazz = greeting.getClass();

なおプリミティブ型をClassオブジェクトとして表現する場合は、当該ラッパークラスのTYPEフィールドを使用します。
例えばint型であればInteger.TYPEになります。

ClassクラスのAPI

Classクラスには数多くのAPIがありますが、本コースではそれらを以下のように分類します。

(1)クラスの名前など基本的な情報を取得するためのAPI
(2)キャストするためのAPI
(3)リフレクションのためのAPI
(4)アノテーションを取得するためのAPI

ここではまず、この中から(1)および(2)の主要なAPIについて示します。

API(メソッド) 説明
String getCanonicalName() このクラスの正規名を返す。
String getSimpleName() このクラスの単純名を返す。
String getPackageName() このクラスのパッケージ名を返す。
ClassLoader getClassLoader() このクラスをロードしたクラスローターを返す。
T cast(Object) 指定されたオブジェクトを、このClassオブジェクトが表すクラスまたはインタフェースにキャストする。

これらのAPIのうち、クラスの情報を取得するためのものを具体的に見ていきましょう。

snippet_3 (pro.kensait.java.advanced.lsn_8_1_2.Main)
Class<Greeting> clazz = Greeting.class;
System.out.println(clazz.getCanonicalName()); //【1】
System.out.println(clazz.getSimpleName()); //【2】
System.out.println(clazz.getPackageName()); //【3】

getCanonicalName()メソッドにより、クラスの正規名として"pro.kensait.java.advanced.lsn_8_1_2.Greeting"が返ります【1】。正規名はFQCNとは厳密には異なりますが、入れ子クラスではない、トップレベルのクラスおよびインタフェースでは、正規名=FQCNになります。
getSimpleName()メソッドにより、クラスの単純名として"Greeting"が返ります【2】。
getPackageName()メソッドにより、このクラスのパッケージ名として"pro.kensait.java.advanced.lsn_9_1_2"が返ります【3】。
次にキャスト(ダウンキャスト)です。
クラスのダウンキャストは、キャスト演算子によって以下のように行いますが、

Integer val = (Integer) obj;

Classクラスが持つcast()メソッドによって、以下のように行うことも可能です。

snippet_4 (pro.kensait.java.advanced.lsn_8_1_2.Main)
Integer val = Integer.class.cast(obj);

このチャプターで学んだこと

このチャプターでは、以下のことを学びました。

  1. クラスのライフサイクル(ロード、インスタンス生成、破棄)や、クラスローダー、ガベージコレクションの機能について。
  2. クラスローダの種類と階層構造、クラスがロードされるタイミングについて。
  3. コンテキスト・クラスローダーによるリソースの読み込み方法について。
  4. Classクラスの特徴やAPIについて。

Discussion