IBM®
本文へジャンプ
    Japan [変更]    ご利用条件
 
 
検索範囲検索:    
    ホーム    製品    サービス & ソリューション    サポート & ダウンロード    マイアカウント    
skip to main content

developerWorks Japan  >  Java technology  >

Javaプログラミングのダイナミックス 第1回:クラスとクラスのロード処理

クラスおよびJVMがクラスをロードするときの動作についての研究

developerWorks
ページオプション

JavaScript を要するドキュメントオプションは表示されません

原文はこちら

原文はこちら


レベル: 中級

Dennis Sosnoski (dms@sosnoski.com), President, Sosnoski Software Solutions, Inc.

2003年 4月 29日

Javaプログラミングのダイナミックな側面に光を当てるこの新しいシリーズで、Javaアプリケーションを実行する際に舞台裏でどんなことが行われているのかを学んでみましょう。エンタープライズJavaの専門家であるDennisSosnoski氏が、Javaのバイナリー・クラス・フォーマットやJVM内部でのクラスの扱いについて未聞の情報を紹介してくれます。それと同時に、簡単なJavaアプリケーションを実行するのに必要なクラスの数から始まり、J2EEなどの複雑なアーキテクチャーで問題を起こしたりするクラス・ローダーの衝突に至るまで、クラスのロード処理の問題についても説明してくれます。

本稿は、私がJavaプログラミングのダイナミックス と呼んでいる一連のテーマを紹介していく新しいシリーズの第1回目です。これらのテーマには、Javaバイナリー・クラス・ファイル・フォーマットの基本的な構造に始まり、リフレクションを使用しての実行時メタデータ・アクセス、さらには、実行時に新しいクラスを修正したり構築する方法に至るまでの内容が含まれます。これらの題材すべてに共通するのは、Javaプラットフォームのプログラミングが、素直にネイティブ・コードにコンパイルされる言語を扱う場合よりも、はるかにダイナミックであるという考えです。こうしたダイナミックな側面を理解できれば、主流となっている他のどんなプログラミング言語を使ってもかなわないようなことをJavaプログラミングで行えるようになります。

今回は、Javaプラットフォームのこうしたダイナミックな性質の基盤となっている基本的な概念をいくつか紹介したいと思います。これらの概念を、クラスがJVMにロードされる際にどんなことが行われるのかなど、Javaクラスの表現に用いられるバイナリー・フォーマットの観点から展開していきます。このテーマは、本シリーズの次回以降の記事の基礎となるだけでなく、Javaプラットフォームで開発を行う技術者にとって非常に実際的な問題もいくつか明らかにします。

クラスのバイナリー形式

Java言語で開発を行っている技術者は、通常、ソース・コードがコンパイラーにかけられるときどんな処理を受けるのか細かいことまで関知する必要はありませんが、私はこのシリーズで、ソース・コードからプログラムの実行に至るまでの舞台裏の詳細をいろいろと紹介したいと考えています。そこでまず、コンパイラーによって生成されるバイナリー・クラスについて研究してみたいと思います。

バイナリー・クラス・フォーマットを実際に定義しているのは、JVMの仕様です。通常これらのクラス表現は、Java言語のソース・コードを基にコンパイラーによって生成され、通常.class という拡張子のファイルに保存されます。しかし、これらの性質はいずれも必須だというわけではありません。Javaバイナリー・クラス・フォーマットを利用するプログラミング言語が他にも開発されてきていますし、いろいろな目的から、新しいクラス表現が構築され、実行中のJVMに直接ロードされたりもしています。JVMにとって重要なのは、ソースやソースがどのように保存されているかではなく、フォーマットそのものです。

では、そのクラス・フォーマットは、実際どんなものなのでしょうか。リスト1は、(非常に)短かなクラスのソース・コードと、コンパイラーが出力したクラス・ファイルの16進表示の一部を示したものです。


リスト1. Hello.javaのソースとバイナリー (の一部)
                
public class Hello
{
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}
0000: cafe babe 0000 002e 001a 0a00 0600 0c09  ................
0010: 000d 000e 0800 0f0a 0010 0011 0700 1207  ................
0020: 0013 0100 063c 696e 6974 3e01 0003 2829  .....<init>...()
0030: 5601 0004 436f 6465 0100 046d 6169 6e01  V...Code...main.
0040: 0016 285b 4c6a 6176 612f 6c61 6e67 2f53  ..([Ljava/lang/S
0050: 7472 696e 673b 2956 0c00 0700 0807 0014  tring;)V........
0060: 0c00 1500 1601 000d 4865 6c6c 6f2c 2057  ........Hello, W
0070: 6f72 6c64 2107 0017 0c00 1800 1901 0005  orld!...........
0080: 4865 6c6c 6f01 0010 6a61 7661 2f6c 616e  Hello...java/lan
0090: 672f 4f62 6a65 6374 0100 106a 6176 612f  g/Object...java/
00a0: 6c61 6e67 2f53 7973 7465 6d01 0003 6f75  lang/System...ou
...

バイナリーの内部

リスト1に示したバイナリー・クラス表現で真っ先にきているのが、Javaバイナリー・クラス・フォーマットであることを表すcafebabeというシグニチャーです (ついでに言うと、これは、Javaプラットフォームを構築しようという開発者魂を保ちながら一生懸命がんばったバリスタたち への、いつまでも残るが、広く知られることのない感謝の印にもなっています)。このシグニチャーにより、データ・ブロックがJavaクラス・フォーマットのインスタンスであることを表明している のかどうかを簡単に確認できます。Javaバイナリー・クラスは、ファイル・システム上にないものも含め、すべて、この4バイトで始まっている必要があります。

それ以降のデータは、それほど面白くはありません。シグニチャーの後には、クラス・フォーマットの1対のバージョン番号(上の例では、1.4.1のjavacが生成したコードとして、マイナー・バージョンの0とメジャー・バージョンの46、すなわち16進数の0x2eになっている)が続き、さらに定数プールのエントリー数がきます。エントリー数 (上の例では26、すなわち0x001a)の後には、実際の定数プールのデータがきます。ここには、クラス定義で使用されるすべての定数が格納されます。この中には、各種のバイナリー値の他に、クラスやメソッドの名前、シグニチャー、文字列(文字列は、16進ダンプの右側にテキストとして読むことができます) が含まれます。

定数プールの項目は可変長であり、各項目の第1バイト目は、項目の型およびそのデコード方法を示しています。ここでは、これについて深入りしないことにします。興味のある方は、JVMの仕様を始めとして参考文献がたくさんありますので、そういったものを参照してください。重要なのは、このクラスおよびそのメソッドの実際の定義だけでなく、このクラスで使用される他のクラスやメソッドへの参照もすべて、定数プールに含まれるという点です。定数プールが、バイナリー・クラスのサイズの半分以上を占めることもよく起こります。もっとも、平均的には半分以下なのでしょうが。

定数プールの次には、そのクラス自体、スーパー・クラス、およびインターフェースの定数プールのエントリーを参照する項目がいくつかきます。これらの項目の次には、フィールドやメソッドについての情報が格納されます。フィールドやメソッドは、それ自身、複雑な構造として表現されます。メソッドの実行コードは、メソッド定義の中にコード属性 の形で含まれます。このコードは、JVMに対する命令の形式をしており、一般にバイトコード と呼ばれています。これが、以下の節でとりあげるテーマの1つです。

属性 は、Javaクラス・フォーマットの中で、上で触れたバイトコードやフィールドの定数値、例外処理、デバッグ情報など、定義されたいくつかの目的で使用されます。ただし、これらの目的だけが属性の用途だというわけではありません。JVMは未知の型の属性を無視しなければならないというのが、当初からのJVMの仕様となっています。この要件によって、属性の用途は、フレームワークがユーザー・クラスと連携する際に必要となるメタ情報を提供するなど(Javaから派生したC# 言語が随所で使用してきている手法)、柔軟に拡張することができ、将来他の目的に利用できるようになっています。ただ、残念ながら、ユーザー・レベルでこの柔軟性を利用するためのフックは、まだ実現されていません。




上に戻る


バイトコードとスタック

クラス・ファイルの実行コードの部分を構成しているバイトコードは、JVMという特殊なコンピューターのためのマシン・コードとなっています。これは、もともとハードウェアではなくソフトウェアで実装するために設計されたコンピューターであり、仮想 マシンと呼ばれています。Javaプラットフォーム・アプリケーションを実行するためのJVMは、すべて、このマシンの実装を中心に構築されています。

といっても、この仮想マシンは、非常に簡単なもので、スタック構造を採用しています。すなわち、命令オペランドを使用する場合、まず、それを内部スタックにロードしておきます。命令セットには、通常の算術命令や論理命令がすべて用意されている他、条件分岐、無条件分岐、ロード/ストア、コール/リターン、スタック操作、および特殊な命令がいくつか用意されています。命令の中に即値オペランド(immediate operand values) が直接コード化される命令もあります。その他の命令は、定数プールの値を直接参照します。

仮想マシンの構造が簡単だからといって、必ずしもその実装が簡単だというわけではありません。初期の(第1世代の) JVMは、基本的に、仮想マシンのバイトコードのインタープリターでした。これらのインタープリターは、確かに、比較的単純な ものでしたが、性能に関しては大きな問題がありました。ネイティブ・コードの実行に比べ、コードの解釈実行(interpret) には時間がかかるのが常です。こうした性能上の問題を軽減するために、第2世代のJVMには、ジャストインタイム (JiT) 変換機能が追加されました。JiTとは、Javaのバイトコードを一番最初に実行するときに、それをまずネイティブ・コードにコンパイルしておくことで、繰り返し実行されるときの性能を格段に向上させる技法のことです。現在の世代のJVMは、さらに進歩しており、プログラムの実行を監視し、頻繁に使用されるコードを選択的に最適化するという適応的手法を採用しています。




上に戻る


クラスのロード

コンパイルでネイティブ・コードを作成するCやC++ などの言語は、通常、ソース・コードをコンパイルした後、リンクを行う必要があります。このリンク処理では、ソース・ファイルを別々にコンパイルして得られたコードおよび共有のライブラリー・コードをマージして、1個の実行プログラムが作成されます。しかし、Java言語は違います。Java言語の場合、コンパイラーで生成されたクラスは、通常、JVMにロードされるまでは、そのままにされます。クラス・ファイルからJARファイルを構築する場合でも、これに変わりはありません。JARは、クラス・ファイルのコンテナーにすぎません。

クラスのリンクは、別個の手順としてではなく、JVMがクラスをメモリーにロードする際の処理の一部として行われます。この場合、クラスを最初にロードする際に、ある程度のオーバーヘッドが生じますが、Javaアプリケーションには高度な柔軟性をもたらします。たとえば、実装コードが実行時まで特定されなくても、そのコードへのインターフェースを使ってアプリケーションを記述することができます。アプリケーションを組み立てるときの、この実行時バインディング (late binding) という手法は、Javaプラットフォームでは随所で使用されており、サーブレットがそのよい例です。

クラスをロードする際の規則は、JVMの仕様に詳しく明記されています。基本的な原則として、クラスは、必要になったときに初めてロードされます(あるいは、少なくともそのような方法でロードされるように見えます。実際のロード方法に関しては、JVMがある程度柔軟に処理できますが、クラスの初期化に関しては、決まった手順に従う必要があります)。ロードされるクラスが別のクラスに依存していたりしますので、ロード処理は再帰的に行われます。リスト2のいくつかのクラスは、この再帰的なロードの動作原理を示しています。Demo クラスは、Greeter のインスタンスを作成し、greet メソッドを呼び出す簡単なmain メソッドを含んでいます。Greeter のコンストラクターは、Message のインスタンスを作成し、それをgreet メソッド呼び出しの中で使用しています。


リスト2. クラスのロード処理を説明するためのソース・コード
                
public class Demo
{
    public static void main(String[] args) {
        System.out.println("**beginning execution**");
        Greeter greeter = new Greeter();
        System.out.println("**created Greeter**");
        greeter.greet();
    }
}
public class Greeter
{
    private static Message s_message = new Message("Hello, World!");
    public void greet() {
        s_message.print(System.out);
    }
}
public class Message
{
    private String m_text;
    public Message(String text) {
        m_text = text;
    }
    public void print(java.io.PrintStream ps) {
        ps.println(m_text);
    }
}

java のコマンド・ラインに-verbose:class というパラメーターを指定すると、クラスのロード処理のトレースを表示することができます。リスト3は、このパラメーターを指定してリスト2のプログラムを実行したときの出力の一部です。


リスト3. -verbose:classによる出力の一部
                
[Opened /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Opened /usr/java/j2sdk1.4.1/jre/lib/sunrsasign.jar]
[Opened /usr/java/j2sdk1.4.1/jre/lib/jsse.jar]
[Opened /usr/java/j2sdk1.4.1/jre/lib/jce.jar]
[Opened /usr/java/j2sdk1.4.1/jre/lib/charsets.jar]
[Loaded java.lang.Object from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.io.Serializable from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.lang.Comparable from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.lang.CharSequence from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.lang.String from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
...
[Loaded java.security.Principal from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.security.cert.Certificate from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded Demo]
**beginning execution**
[Loaded Greeter]
[Loaded Message]
**created Greeter**
Hello, World!
[Loaded java.util.HashMap$KeySet from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.util.HashMap$Keyiterator from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]

ここには、最も重要な部分だけを示してあります。トレース全体では294行になりますので、このリストは、そのほとんどを削除したものになっています。クラスのロードの最初の方(上の例では279回) は、すべて、Demo クラスをロードしようとすることで起動されています。これらのクラスは、どんなに小さなJavaプログラムであれ、すべてのプログラムが使用するコアのクラス群です。Demoのmain メソッドからすべてのコードを削除したとしても、このロードの初期の手順は変わりません。ただし、ロードされるクラスの数と名前は、クラス・ライブラリーのバージョンによって違ってきます。

Demo クラスをロードした後の部分のリストからは、もっと面白いことがわかります。ここに示されている手順から、Greeter クラスは、そのインスタンスが作成される段になって初めてロードされていることがわかります。一方、Greeter クラスは、Message クラスの静的インスタンスを使用していますので、前者のインスタンスを作成するためには、後者のクラスもロードする必要があります。

クラスがロードされ初期化されるとき、JVM内部ではいろいろなことが行われます。バイナリー・クラス・フォーマットのデコード、他のクラスとの互換性のチェック、バイトコード命令のシーケンスの検査などが行われ、最後には、新しいクラスを表現するためのjava.lang.Class インスタンスが構築されます。このClass オブジェクトは、JVMで新たに作成されるクラスのすべてのインスタンスの基礎となります。また、Class オブジェクトは、ロードされたクラスそのもののiDでもあります (JVMにロードされる同じバイナリー・クラスから複数のコピーを設けることができるわけですが、それぞれのコピーに固有のClass インスタンスが生じます)。これらのコピーは、すべて同じクラス名を共有するにもかかわらず、JVMにとっては別々のクラスとして扱われます。

変わり種の (クラス) パス

JVMでのクラス・ロード処理は、クラス・ローダー によって制御されます。JVMには、Javaクラス・ライブラリーの基本的なクラス群をロードするためのブートストラップ・クラス・ローダーが内蔵されています。このクラス・ローダーは、特別な機能を備えています。1つは、ブート・クラス・パスに存在するクラスだけをロードするということです。これらのクラスは信頼できるシステム・クラスですので、ブートストラップ・ローダーは、通常の(信頼できない) クラスに対して行われる妥当性チェックをほとんど省略します。

クラス・ローダーは、ブートストラップだけではありません。たとえば、JVMは、標準的なJava拡張APiによってクラスをロードするための拡張 クラス・ローダーや、一般的なクラスパスから (アプリケーション・クラスなどの)クラスをロードするためのシステム・クラス・ローダーを定義しています。アプリケーションも、(クラスを実行時に再ロードするなどの)特別な用途に、独自のクラス・ローダーを定義することができます。そうしたクラス・ローダーを追加する場合、java.lang.ClassLoader クラスを派生させたもの (間接的な派生も含めて) が使われます。これは、バイト配列から内部的なクラス表現(java.lang.Class インスタンス) を構築するときの中核的なサポート機能を提供するクラスです。構築されたクラスは、ある意味では、それをロードしたクラス・ローダーが「所有」しています。クラス・ローダーは、通常、ロードしたクラスのマップを作成し、次に要求があった場合には名前でクラスを検索できるようにします。

また、各クラス・ローダーは、親のクラス・ローダーへの参照も記録し、ブートストラップ・ローダーをルートとするクラス・ローダーのツリーを定義していきます。あるクラス(名前で識別される) のインスタンスが要求された場合、その要求を最初に処理するクラス・ローダーは、通常、直接そのクラスをロードせずに、まずその親のクラス・ローダーを最初にチェックします。クラス・ローダーが複数の階層になっている場合、これが再帰的に適用されていきます。したがって、クラスは通常、それをロードしたクラス・ローダーだけから見える のではなく、それより下位のすべてのクラス・ローダーにも見えることになります。また、クラスがチェーンの中の複数のクラス・ローダーによってロード可能な場合、ツリーの中の一番上に位置するクラス・ローダーが、そのクラスを実際にロードすることになります。

Javaプログラムによって複数のアプリケーション・クラス・ローダーが使用される状況には、いろいろな場合があります。たとえば、J2EEフレームワークがその例です。フレームワークによってロードされるJ2EEアプリケーションには、それぞれ別個のクラス・ローダーを用意してやり、あるアプリケーションのクラス群が別のアプリケーションに干渉しないようにする必要があります。フレームワークのコード自体も、独自のクラス・ローダーを何個か使用し、やはりアプリケーションとの間で干渉を起こさないようにします。このようにして、クラス・ローダーの全体は、各レベルでロードされるクラスの種類が異なるような、ツリー構造の階層を構成します。

ローダーのツリー

クラス・ローダー階層の実例として、図1に、Tomcatサーブレット・エンジンで定義されているクラス・ローダー階層を示します。この例では、Tomcatインストールの中のあるディレクトリーに入っているJARファイルから、サーバーとすべてのWebアプリケーションの間でコードを共有するためのCommonというクラス・ローダーがロードされます。CatalinaはTomcat自身のクラスをロードするためのローダーで、SharedはWebアプリケーション同士で共有されるクラスをロードするためのローダーです。そして最後に、Webアプリケーションは、それぞれのプライベートなクラスをロードするための独自のローダーを獲得します。


図1. Tomcatのクラス・ローダー群
Tomcatのクラス・ローダー群

このような環境では、新しいクラスを要求する際にどのローダーを使用すべきなのかを管理しておくのが繁雑になったりします。そのため、Java2プラットフォームでは、java.lang.Thread クラスにsetContextClassLoader メソッドとgetContextClassLoader メソッドが追加されました。これらのメソッドを利用することで、フレームワークは、アプリケーションのコードを実行させながら、それぞれのアプリケーションに使用すべきクラス・ローダーを設定することができます。

独立なクラス集合をロードできるという柔軟性は、Javaプラットフォームの重要な機能です。ただし、便利な機能ではあるのですが、場合によっては、混乱を招くこともあります。混乱を招きやすいことの1つに、JVMのクラスパスの扱いというなかなか決着の付かない問題があります。たとえば、図1に示したTomcatのクラス・ローダー階層の場合、クラス・ローダーCommonによってロードされたクラスは、Webアプリケーションでロードされたクラスを直接(名前で) アクセスすることはできません。これらのクラスを結び付けるには、両方のクラス集合から見えるインターフェースを使用するしかありません。この場合、Javaサーブレットによって実装されたjavax.servlet.Servlet もそれに含まれます。

また、何らかの理由でクラス・ローダー間でコードが移動される場合にも、問題が発生する場合があります。たとえば、J2SE1.4がXML処理用のJAXP APiを標準ディストリビューションに移したときには、それまでアプリケーションが独自にXMLAPiの実装を選択してロードするようにしていた数多くの環境で問題が発生しました。そうしたロードは、J2SE1.3では、適切なJARファイルをユーザー・クラス・パスにインクルードすることで可能になっています。J2SE1.4では、これらのAPiの標準バージョンが拡張クラス・パスに配置されるようになりましたので、通常、これらのAPiが、ユーザー・クラス・パスに存在する実装をオーバーライドすることになります。

他にも、複数のクラス・ローダーを使用する場合、混乱を起こす場合があります。図2は、インターフェースとそれに結合される実装が、それぞれ、別個の2つのクラス・ローダーによってロードされる場合に起こるクラス識別の危機 の例を示したものです。インターフェースやクラスの名前やバイナリー実装は同じでも、あるローダーでロードされたクラスのインスタンスが別のローダーでロードされたインターフェースを実装しているものとは認識されません。こうした混乱は、図2のように、インターフェース・クラスi をSystemクラス・ローダーの空間に移してやることで解決できます。このようにしても、依然としてクラスA のインスタンスは別々に2つ存在するわけですが、両方が同じインターフェースi を実装することになります。


図2. クラス識別の危機
クラス識別の危機



上に戻る


まとめ

Javaのクラス定義とJVMの仕様の組み合わせは、コードの実行時組み立てのための極めて強力なフレームワークを定義しています。クラス・ローダーを使用することで、Javaアプリケーションは、いろいろなバージョンのクラスを衝突させないようにして扱うことができます。クラス・ローダーの柔軟性のおかげで、アプリケーションの実行中に、修正したコードを動的にロードし直すことすら可能です。

Javaプラットフォームのこの面での柔軟性に対するコストは、アプリケーションを起動する際に、いくぶんオーバーヘッドが大きくなることです。ごく簡単なアプリケーション・コードの実行を開始する場合でも、JVMは、何百種類ものクラスをロードする必要があります。このような起動時のコストから、Javaプラットフォームは、一般に、頻繁に使用される小さなプログラムよりも、長い間実行されるサーバー・タイプのアプリケーションに適していることになります。また、コードの実行時組み立てという柔軟性を最もよく活用できるのがサーバー・アプリケーションですので、この種の開発でJavaプラットフォームの利用が増加しているのも不思議ではありません。

このシリーズの第2回目では、Javaプラットフォームの動的な性質の土台となっているもう一つの側面であるリフレクションAPiを紹介したいと思います。リフレクションを利用することで、実行コードから、内部的なクラス情報をアクセスできるようになります。クラス同士をソース・コードでリンクすることなく、実行時にまとめてフックすることのできる柔軟なコードを構築したい場合、リフレクションが優れた働きをします。しかしながら、ほとんどのツールと同様、どんなときに、どんな方法でそれを最も上手に活用できるのかを知っておく必要があります。Javaプログラミングのダイナミックス の第2回では効果的なリフレクションの技とそのトレードオフを紹介します。またお会いしましょう。



参考文献



著者について

Photo of Dennis Sosnoski

Dennis Sosnoskiはシアトル地域にあるJava技術のコンサルティング会社、Sosnoski Software Solutions, Inc.の創立者で、主席コンサルタントでもあり、またXMLやWebサービスに関するトレーニングやコンサルティングの専門家でもあります。彼のプロとしてのソフトウェア開発経験は30年以上に渡り、ここ数年はサーバー側のXML技術やJava技術に注力しています。Dennisは、全米各地で行われる会議で頻繁に講演を行っています。また、Javaクラスワーキング技術を基に構築された、オープンソースの JiBX XML Data Binding フレームワークの中心開発者でもあります。




記事の評価


サイト改善のため、ご意見をお寄せください。こちらのフォームからお願いいたします。



 


 


不充分・不完全である大変素晴らしい
 


この記事を共有する

del.icio.us del.icio.us newsing newsing FC2ブックマーク FC2ブックマーク
Choix! Choix! ニフティクリップ ニフティクリップ Yahoo!ブックマーク Yahoo!ブックマーク
MM/memo MM/memo CZブックマーク CZブックマーク livedoorクリップ livedoorクリップ
はてなブックマーク はてなブックマーク Buzzurl(バザール) Buzzurl(バザール)




上に戻る


    日本IBMについて プライバシー お問い合わせ