コード・チューニング

Revised: Feb./23rd/2003

ここでは、Javaアプリケーション・ソフトウェアの実行環境とその動作についてかいつまんで説明します。これらは、クラスの選択基準に関する一般論を提供します。

Javaの実行

アプリケーションがJavaVM内でどのように実行されるのかをイメージできると、クラス選択の幅が広がります。

Javaアプリケーションは、OSに導入されたJavaVM (Java Virtual Machine)と呼ばれるソフトウェアで実行されます。WindowsでもUNIX/Linuxでも、導入されたJavaVMの動作は同じなので、アプリケーション側でOSや実行CPUのアーキテクチャを意識する必要はありません。もちろん、JavaVM自体はOS毎に異なるのですが、OS毎に導入されたJavaVMが、アプリケーションに対してOSの差異を吸収してくれるわけです。

JavaVMを導入する製品は、Sun Microsystemsの他にいろいろなところで開発されています。しかし、その仕様はSun Microsystemsが公開しており、異なる製品であっても、共通の思想で設計されています。

JavaVMはOSの真似事として、メモリ管理を実行します。アプリケーションをメモリ上にロードし、実行し、済んだら当該メモリ領域を解放して再利用可能にします。そのために、JavaVM自身が占有するメモリ領域内には、本当のOSがするように、アプリケーションを実行するための領域が作成されます。

OS上でプロセスとして実行されるJavaVMは、アプリケーションをマルチスレッドで実行します。一つのアプリケーションは最低でも一つのスレッドを持ち、スレッドを生成する度にJavaスタックと呼ばれる領域をメモリ上に作ります。スタックには、当該スレッド内で呼び出されるメソッドや変数、クラス、オブジェクトへのポインタを、後入れ先出し方式(LIFO: Last In First Out)で積み重ねます。

スタックされたポインタが指す実体は、スレッド間で共有されるヒープと呼ばれる領域に保持します。ヒープは木構造のデータ構造で、探索を高速にするための工夫です。オブジェクトへの参照を代入する参照型変数が持つ値は、最も単純な場合には、スタック上に積み上げられた、ヒープ内のオブジェクト(型とメソッド、及びデータ)を取り扱うハンドルのポインタへのポインタを表すことが多いでしょう。

全てのスタックからポインタが消滅すると、ヒープ上の対応するデータは無意味になります。これをかき集めて再利用可能にするために、JavaVM内ではガベッジ・コレクション(ごみ集め)と呼ばれるオーバーヘッド処理が実行されています。

コードのチューニング

Javaはオブジェクト指向の言語ですが、プロシージャ指向の言語と同様、アルゴリズムやクラスを交換することで、パフォーマンスが向上する場合があります。

プロシージャ指向の言語では、ステップ数を減らすことが、コードレベルのチューニングでした。オブジェクト指向では、メソッドの呼び出しとオブジェクトの生成回数を減らすことが第一段階になります。

オブジェクトの生成では、制御の流れとしては、クラスの継承階層を下る順番でコンストラクタが実行され、逐次的に初期化されます。このとき、ヒープ内へのインスタンスの作成、スタックへのポインタの登録などが実行されます。呼び出しでは、ポインタをスタックに積んで対応するヒープ内を探索します。メモリ領域の解放では、ガベッジ・コレクタが実行されます。ここでは、メモリ領域の構成を詳細に述べる余裕はありませんが、オブジェクトの利用はコストの高い処理になります。

生成されるバイト・コードからステップ数を確認することもできます。例えば、リスト13のコードを考えてみましょう。

▼リスト13

class ByteCodeDemo {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            if (i%2 != 0) {
                System.out.println(i + " is odd!");
            } else {
                System.out.println(i + " is even!");
            }
        }
    }
}

コンパイルにより生成されたクラスファイルのバイト・コードは、リスト14のように出力されます。

▼リスト14

> javac ByteCodeDemo.java
> javap -c ByteCodeDemo
Compiled from ByteCodeDemo.java
class ByteCodeDemo extends java.lang.Object {
    ByteCodeDemo();
    public static void main(java.lang.String[]);
}
Method ByteCodeDemo()
   0 aload_0
   1 invokespecial #1 
   4 return
   
Method void main(java.lang.String[])
   0 iconst_0
   1 istore_1
   2 goto 67
   5 iload_1
   6 iconst_2
   7 irem
   8 ifeq 39
  11 getstatic #2 
  14 new #3 
  17 dup
  18 invokespecial #4 
  21 iload_1
  22 invokevirtual #5 
  25 ldc #6 
  27 invokevirtual #7 
  30 invokevirtual #8 
  33 invokevirtual #9 
  36 goto 64
  39 getstatic #2 
  42 new #3 
  45 dup
  46 invokespecial #4 
  49 iload_1
  50 invokevirtual #5 
  53 ldc #10 
  55 invokevirtual #7 
  58 invokevirtual #8 
  61 invokevirtual #9 
  64 iinc 1 1
  67 iload_1
  68 bipush 10
  70 if_icmplt 5
  73 return

ここではバイト・コードの詳細は説明しませんが、デフォルトのコンストラクタの生成、文字列連結時のStringBufferの利用、ブロックのGOTO文による実装、ステップ数など、さまざまな情報が得られます。

「Javaは遅い」とよく言われます。しかし、JavaVMのパフォーマンスも向上しています。遅さの原因は、JavaVMのチューニング不足と、クラスの選択やオブジェクトの利用といったコードの設計不備が原因であるかもしれません。

パフォーマンス・チューンという話題では、JavaVMやアプリケーション・サーバのパラメタ指定など、ミドルウェア設計に関心が集中する傾向があります。もちろん重要なのですが、プロシージャ指向の言語と同様、Javaでもアプリケーション・デザインは重要です。モデリングの時点で、ロジックのアルゴリズム選択、可読性や保守性、再利用可能性、拡張可能性に加えて、利用するクラス毎のパフォーマンス特性も考慮する必要があります。時にはパフォーマンス追求とオブジェクト指向設計が矛盾することもあり、いずれを選ぶかはトレードオフになります。

オブジェクト指向言語はモジュール化を促進する言語でもあります。この特性を活かして、自分が開発するクラス内ではパフォーマンスについても配慮してください。そのためには、現実的な負荷環境の用意と、モジュール開発局面内でのベンチマーク・テストが必要です。



Copyright © 2003 SUGAI, Manabu. All Rights Reserved.