J2EEがミッションクリティカルな分野に適用されるようになり、Javaのパフォーマンスチューニングの重要性はさらに高まっています。パフォーマンスチューニングにはさまざまなパラメータがありますが、中でもJava VMに関連するチューニングの効果は大きいといわれています。本稿は、Java VMに関連するチューニング手法を学ぶための前提知識を提供することを目的にしています(編集部)。
本記事は2004年に執筆されたものです。Javaチューニング全般の最新情報は@IT キーワードINDEXの「Javaパフォーマンス管理」をご参照ください。
Java VMに関連するチューニングを行い、J2EEアプリケーションのパフォーマンスを上げるためには、Java VMについて詳しく知る必要があります。本稿は2回に渡ってJava VMの基本構造と動作原理を詳細に解説しますが、内容を理解するためにはプログラムがコンピュータ上で動作する基本原理とJava VMの基本用語を知っている必要があります。Java VMの基本用語に関しては、「実行スピードに挑戦するJavaアーキテクチャの変遷をたどる」(Java Solution)を参考にしてください。また、Itaniumアーキテクチャについては、「明らかになるItanium 2の性能とプラットフォーム」(System Insider)を始めとするSystem Insiderフォーラム内の関連記事を参考にしてください。本連載の終了後には、Java VMに関連するチューニングテクニックを解説する「Java VMパフォーマンステクニック」(仮題)の連載が予定されています。
JavaプログラムはJava VM環境上で動作する
「Write Once, Run Everywhere」という言葉に象徴されるように、Java言語で書いてJavaコンパイラで作成したJavaプログラム(クラスファイル)は、Java VMが存在する環境であればその下のOSやハードウェアが何であろうと、同じ動作をします。これはJava VMがその下位にあるOSやハードウェアの違いを吸収し、その上位にあるJavaプログラムと共通のインターフェイスを提供していることで実現されています。つまり、どんな環境の下でも、Javaプログラムから見ればJava VMという仮想ハードウェア上で動作するように見えるわけです。
Javaコンパイラがバイトコードを生成
Java言語で書かれたソースコードファイルをJavaコンパイラ(javac)でコンパイルすると、Javaバイトコードを含むクラスファイル(.class)が生成されます(図2参照)。Javaコンパイラは、サン・マイクロシステムズのJDK付属のもの以外にも、Java開発環境を販売するサードパーティーのソフトウェアベンダなどからも提供されています。
JavaコンパイラによるJavaソースコードのコンパイルと、C/C++コンパイラによるC/C++ソースコードのコンパイルの対比を図3に示します。これを見ると、Javaが当初インタプリタ方式といわれたとはいえ、開発言語としてはPerlなどよりはC/C++に近いものであることが分かります。ただし、C/C++などの言語で作成したソースコードはOSに依存しますし、C/C++コンパイラは特定のプロセッサ専用のコードを生成します。結果として、C/C++コンパイラで生成した実行ファイルはハードウェア(プロセッサ)とソフトウェア(オペレーティングシステム)の両方に依存することになります。それに対してJavaは、特定のハードウェアやソフトウェアへの依存しないコードを生成します。
バイトコードとJava VMの関係
Javaクラスファイルの中にはそのクラスで定義されているメソッド、フィールド変数、定数などの情報が書かれており、メソッド中にはバイトコードが書き込まれています(図2参照)。バイトコードはその名のとおりオペコードが1バイト固定長で、オペランドは可変長です。
ところで、図4に示すように、Java VM上で実行されるバイトコードは、実プロセッサ上で実行されるマシンコードに対応するものと考えることができます。すなわち、バイトコードにとってJava VMは実プロセッサに該当するものであることから、仮想の実プロセッサという意味でJava仮想マシン(Java Virtual Machine)と呼ばれるわけです。
Java VMはソフトウェアで実装されるため、さまざまなプラットフォームへの移植が可能であり、その結果、Javaバイトコード(つまりJavaプログラム)をさまざまなハードウェア/ソフトウェアプラットフォームで実行させることができます。
Java VMは誰が実装しているのか
Java VMの実装はサン・マイクロシステムズが提供しています。しかし、サン・マイクロシステムズからソースコードライセンスを受けなくても、Java仮想マシン仕様(Java Virtual Machine Specification)を満たせば独自にJava VMを作ることは可能なため、各社が独自のJava VMを作っています。
例えば、IBMのJava VMやBEAのJRockitは独自のJava VMの例です。また、HPのようにサン・マイクロシステムズからソースコードライセンスを受けている場合、プロセッサやOSに依存する部分の実装や、各種パラメータの設定は独自に行っていますが、それ以外の基本部分はサン・マイクロシステムズのコードをそのまま使っています。すなわち、HPのJava VMはサン・マイクロシステムズが提供するJava VMと同一のものということです。現在のJava VMはパフォーマンスを改善したHotSpot VMなので、HPのJava VMはサンのHotSpot VMと基本部分のソースコードは同一です。
Java VMによるJavaプログラムの実行
Java VMは特定のOSとハードウェア上に作られた仮想マシンであり、ソフトウェアにより実装されています。いい換えるとJava VMはJavaの実行環境を提供するソフトウェアエミュレータです。ほかのソフトウェアエミュレータの例としては、Itaniumマシン上のHP-UXでPA-RISC/HP-UXの実行環境を提供するAriesがあります。PA-RISC/HP-UXのアプリケーションをItanium/HP-UXシステム上で実行できるのは、Ariesのおかげです。
Java VM上で動作するJavaプログラムを1つのプロセスとして見ると、Java VMというプログラムが動作しながら、Javaプログラムが同じプロセス内で動作していることになります。Java VMの仮想メモリ上でのイメージは図5のようになります。Javaプログラムを動作させるにはJavaヒープとJava VMスタックが必要になります。Javaヒープにはクラスファイルを展開している領域とJavaクラスのインスタンスが置かれる領域があります。これに対し、Java VM自身もプログラムなので実行テキストに加えてCヒープとスタックを持ちます。命令ポインタもバイトコードの命令を指すものとネイティブコードを指すものが存在します。Javaプログラムを実行しているスレッドでは、プロセッサの命令ポインタは主にインタプリタ内かコードキャッシュ内を指しています。
図5の構造を理解しておけばJavaにおける問題発生時の対応に役立ちます。例えば「メモリ不足」という状況になった場合、Cヒープが足りない、Javaプログラムのヒープが足りない、スタックが足りない、OSのスワップが足りないなどさまざまな原因が考えられます。それぞれの領域の使用目的やこれらをどういったオプションやパラメータで設定できるか、また実際のJava VMがOSのリソースをどのように使用しているかなどを理解しておく必要があります。
Javaヒープとは
クラスファイルはJava VMのクラスローダによりファイルからメモリへとロードされます。HotSpot VMでは、「-verbose:class」を付けて起動するとロードされたクラスを表示してくれます。図6に示すように、HotSpot VMではクラスの静的情報はJavaヒープのPermanent Generation領域に配置されます。静的情報には、メソッド定義(バイトコード)、フィールド定義、定数、および static 宣言されているフィールドの値などが含まれます。Javaにはクラスを動的にロードする機能があるため、Java VMの起動時以外でもクラスをロードすることができます。JSPはこの仕組みを利用しているため、JSPを動作させているJava VMのPermanent Generation領域の使用量は動的に増えます。クラスをインスタンス化すると、生成されたオブジェクトはJavaヒープのNew/Old Generation領域に置かれます。Javaヒープはどちらの領域もガベージコレクションの対象になります。Javaヒープ内部の詳細は次回のガベージコレクションの回で説明します。
Java VMスタックとは
Javaに限らず、一般に関数が1つ呼ばれるたびにスタック上にスタックフレームが1つ積まれます。このスタックフレームの中にはローカル変数と次に呼び出す関数に渡す引数の一部が置かれます。Javaにもスタックがあり、Javaバイトコードインタプリタが利用します。Java VMスタックのスタックフレーム内には、ローカル変数と引数に加えてオペランドスタックが置かれます。オペランドスタックは、Java VMがスタックマシンであるために必要なものです。下記のJavaバイトコードはオペランドスタックを利用した演算の例です。これは、0番目と1番目のローカル変数を、(このフレーム内の)オペランドスタック領域に積み、この2つの値を加算します。
iload_0 iload_1 iadd
HotSpot VMでは、Java VMスタックとネイティブコードのスタックは同じ領域に連続して存在します。つまりJavaプログラムのフレームとネイティブコードのフレームが1つのスタック内に混在することになります。リスト1はJavaプログラムを実行しているJavaスレッドのスタックトレースをHPのgdbで表示させたものです。
フレーム#14から#0に向かって順番に関数が呼び出されていることを示しています。スレッド生成後にいくつかの処理を行い、途中からJavaアプリケーションを実行しているのが分かります。ここで、#14から#8はJava VM自身のフレームで、#7から#0がJavaプログラムのフレームです。
$ gdb /opt/java1.4/bin/IA64N/java -p 4429 HP gdb 4.0 for HP Itanium (32 or 64 bit) and target HP-UX 11.2x. Copyright 1986 - 2001 Free Software Foundation, Inc. Hewlett-Packard Wildebeest 4.0 (based on GDB) is covered by the GNU General Public License. Type "show copying" to see the conditions to change it and/or distribute copies. Type "show warranty" for warranty/support. ..(no debugging symbols found)... Attaching to program: /opt/java1.4/bin/IA64N/java, process 4429 : : : (gdb) bt #0 0x60000000cd762860:0 in os::Hpux::get__lwFastHighResolutionTimer+0x100 () from /opt/java1.4/jre/lib/IA64N/server/libJava VM.so #1 0x60000000cd562a90:0 in Java VM_CurrentTimeMillis+0xe0 () from /opt/java1.4/jre/lib/IA64N/server/libJava VM.so #2 0x200000007a800950 in Java native_call_stub frame warning: The Unwind info header section .IA_64_unwind_hdr is missing Skipping this library /usr/lib/hpux32/libcl.so.1. () #3 0x200000007a867ff0 in compiled frame: java.lang.System::currentTimeMillis () ->long () #4 0x200000007a868c80 in compiled frame: MyLoop::method2 (int) ->void MyLoop::method1 () ->void () #5 0x200000007a8683e0 in i2c_adapter frame () #6 0x200000007a80da10 in interpreted frame: MyLoop::method1 () ->void () #7 0x200000007a802f10 in interpreted frame: MyLoop::main (java.lang.String[]) ->void () #8 0x200000007a800380 in Java entry frame () #9 0x60000000cd448280:0 in JavaCalls::call_helper+0x2c0 () from /opt/java1.4/jre/lib/IA64N/server/libJava VM.so #10 0x60000000cd762740:0 in os::os_exception_wrapper+0x40 () from /opt/java1.4/jre/lib/IA64N/server/libJava VM.so #11 0x60000000cd446e60:0 in JavaCalls::call+0x90 () from /opt/java1.4/jre/lib/IA64N/server/libJava VM.so #12 0x60000000cd46dc70:0 in jni_invoke_static+0x6e0 () from /opt/java1.4/jre/lib/IA64N/server/libJava VM.so #13 0x60000000cd4ac7d0:0 in jni_CallStaticVoidMethod+0x4e0 () from /opt/java1.4/jre/lib/IA64N/server/libJava VM.so #14 0x5fb0:0 in main+0x1ab0 ()
#7にあるJavaのMyLoopクラスのmain()メソッドはinterpreted frameと表示されており、main()メソッドがインタプリタで実行していることが分かります。
#5のi2c_adapterは、インタプリタで実行されている関数からコンパイルされている関数を呼び出す際に呼ばれるアダプタ関数で、表示されてはいませんが、c2i_adapterはコンパイルされた関数からインタプリタで実行される関数を呼び出すアダプタ関数です。これらアダプタは主に呼び出す関数の引数の順番を変更します。#6も#5と同様、#4を呼び出すためのスタブ的な役割をします。
#4はcompiled frameと表示されており、JavaのMyLoopクラスのmethod1()メソッドとmethod2()メソッドのコンパイルされたネイティブコードが実行されています。2つのメソッドが1つのフレームに表示されているのはmethod2()がmethod1()にインライン展開されていることを示しています。#3も同様にコンパイルされたメソッドのフレームです。
#2はネイティブメソッドを呼び出すためのスタブで、#1と#0ではネイティブメソッドを実行しています。
JavaスレッドとJava VMスタック
初期のJava VMではJava VM内部でスレッド機能をユーザーレベルスレッドとして提供(green_thread)するのがありましたが、HotSpot VMでは、1つのJavaスレッドはそのJava VMの下で動作するOSのスレッドに対応します(native_thread)。UNIXであれば、Javaプログラム実行中にSIGQUITを送って、Windowsであればターミナル上でctrl-breakキーによりスレッドのダンプを表示させることができます。
スタックはスレッドごとに存在します。これらのスレッドは2つのグループに分けることができます。1つはJavaスレッドで、もう1つはJavaシステムスレッドです(図8参照)。JavaスレッドはJavaのアプリケーションを実行しているスレッドですが、JavaシステムスレッドはJavaの実行環境を提供します。インタプリタで実行した関数のスタックフレームのみは、ネイティブコード用に決められているコーリングコンベンション(関数呼び出し規則)と異なるフレームフォーマットになります。リスト1で示したJavaスレッドのスタックトレース例は、図8のstack1に当たります。Javaスレッドのスタックのサイズは、-Xssオプションで指定することができます。このサイズを超えてスタックを伸ばすとJavaの StackOverflowException が通知されます。
Javaシステムスレッドには、ThreadHelper、VMThread、ReferenceHandler、Finalizer、WatcherThread、SuspendCheckerThread、SignalDispatcher、CompilerThreadなどがあります。この中でもReferenceHandlerとFinalizerはJavaで実装されており、図8のstack3に当たります。
Copyright © ITmedia, Inc. All Rights Reserved.