Java実行環境
実行環境の選定
- Javaは発展途上の技術であることに注意する
- メジャーなOSとハードウェアを選ぶ (例: Solaris for SPARC, Windows, Linuxなど)
- 新バージョンのリリースが早い
- バグ修正が早い
- 高性能であることが多い
- できる限り新しいバージョンのJava VMを使用する
- まだバグが多い
- JDK 1.2〜1.2.1はDNSへのアクセスでデッドロックする
- ただし,問題が起きないバージョンのJava VMは保存しておく
- できれば他の選択枝が選べる環境がよい (逃げ道の確保)
- 複数のOSが動くハードウェア
- 複数のJava VMが使用できるOSまたはハードウェア
- 細かな注意点は後述する
ガベージコレクション(GC)
- GCは,Servletシステムの性能を決定する重要な要素である.
- プログラマが直接メモリ管理するより,遅いとは限らない
- ただし,GCアルゴリズムや実装により,すべての場合をカバーできるとは限らない→問題の発生
- 問題例
- GCが頻繁に発生し,ロードアベレージが大きくなる
- GCに時間が掛かり,すべてのリクエストの処理が一時停止する
- GCの注意点
- 適切なGCアルゴリズム (実行環境決定時)
- GCを意識したコーディング (実装時)
- 適切なヒープ管理 (運用時)
GCアルゴリズム
- Java VMで使用されている3種類のGCアルゴリズム
- リファレンスカウント式GC (reference counting GC)…Microsoft VM
- オブジェクトの参照数を計算し,参照数が0のオブジェクトを回収する.
- 利点
- 欠点
- 参照が巡回しているオブジェクトは回収できない.
- カウンタ用のメモリ領域が必要.
- マーク・スイープ式GC (mark and sweep GC)…Classic VM
- 他のスレッドを停止し,GCを以下の順で実行
- 使用されているオブジェクトをマークする.
- マークされていないオブジェクトを削除する
- (ヒープを圧縮する)
- 利点
- 欠点
- ヒープサイズに応じてGC時間が増大する
- スラッシング(thrashing)…ヒープの残量が少ない時には,GCを頻繁に繰り返してしまう
- 世代別コピー式GC (generational copying GC)…HotSpot VM, Exact VM(Java 2 SDK 1.2 for Solaris).
- オブジェクト指向プログラムでは,大半のオブジェクトは生成後すぐ捨てられる
- ヒープをold generationとyoung generationに分割する.
- young generationのヒープ領域でGCを頻繁におこなう.
- ある程度生き残ったオブジェクトはold generationに移動する.
- 利点
- young generationのGCの停止時間が短い
- メモリ割り当てが高速
- 仮想記憶との相性が良い
- 欠点
- old generationのGCの停止時間が長い (Train algorithmを用いているHotSpot VMの場合は短い)
- GCアルゴリズムにより計算量が異なり,リクエスト集中時のロードアベレージに大きく影響する.
- 世代別コピー式GCを採用しているJava VMが一番適している.
クラスのGC
- オートリロードを無効にした場合には,クラスのGCは不要である.
- ヒープのフラグメンテーションを引き起こす可能性がある.
- -Xnoclassgc (または-noclassgc)オプションを使用して,無効にしておくとよい.
ヒープ最大サイズ
- ヒープ最大サイズは,基本的にオブジェクト・レイアウトやヒープ・レイアウトに依存する
- OSやJava VMのアドレスのビット数
- OSの64bit化は進みつつある (例, Solaris 7 for SPARC)
- ただし,ほとんどのJava VMがまだ32bit
- オブジェクト・ポインタのビット数がヒープ最大サイズに直接関係する
- 32bitフルに使わずに,上位ビットの一部を別の目的に使用していることがある…Classic VM
- オブジェクト・ポインタが32bitでも,他との関係からフルにメモリ空間を使用できるとは限らない
- Java 2 SDK for Solarisで2GB以下
- ヒープ最大サイズが問題になるシステムでは,事前に調査しておくとよい.
ヒープの拡張
- GC後に,ヒープ残量が少ない場合
- finalizationを実行→GC
- ReferenceObjectを消去→GC
- 最後にヒープを拡張
- 指定方法
- 最小ヒープサイズ…Java VMの-Xmsオプション (or -msオプション)
- ヒープ拡張単位量…Java VMの-Xmsオプション (or -msオプション)
- 最大ヒープサイズ…Java VMの-Xmxオプション (or -mxオプション)
- 問題: OutOfMemoryErrorの発生
- Classic VMではヒープ残量をハンドルスペース残量で判断するので,必ずしも正しく判断できるとは限らない
- 最大ヒープサイズまで余裕があっても,OutOfMemoryErrorが発生することがある.
- 対策: 最小ヒープサイズを増加する,または最大ヒープサイズと同じ数値(ヒープ拡張を無効にする)にする
- 問題: スラッシングの発生
- ヒープ残量が少ない場合に,ヒープを拡張せずにGCを頻繁に繰り返す…ロードアベレージの増加
- 対策: 最小ヒープサイズを増加する,または最大ヒープサイズと同じ数値(ヒープ拡張を無効にする)にする
- Servletシステムでは,ヒープのモニタは頻繁におこなうべきである
- ヒープ最大サイズを予想より多めに指定する.
- どの程度まで使用するかを監視する.
ヒープの縮小
- 一旦拡張したヒープを縮小するタイミングの判断は困難…スラッシングの可能性
- システム動作中にヒープが縮小されるかはシステムの稼働状況と,Java VMによって異なる
- できれば,低稼働時にヒープが縮小するJava VMが望ましい
- システムの高稼働時…14:00〜17:00, 21:00〜2:00
- システムの低稼働時…4:00〜7:00
ジャスト・イン・タイム・コンパイラ
- できる限り高速なコードを生成するJITコンパイラが望ましい.
- コンパイル時間は,通常アプリケーションほど重視されない
- HotSpotでは-serverオプションを指定する.
- 一度起動したら長時間動作し続けることが多いので,実行時間には影響しない.
- ただし,コンパイル時間が長いほど,システムの(再)起動時間が長くなる.
- Javaプログラムが遅い主な原因
- オブジェクト指向プログラムでは,呼び出すメソッドが動的に決定される.
- 実行時に毎回メソッド探索をするオーバヘッドが大きい→ループでは処理時間のオーダーが数桁異なることもある
- Javaはすべて(非決定的な)メソッド呼び出しで統一されており,CやC++のような(決定的な)関数呼び出しを使うことはできない.→コンパイラが呼び出すメソッドを予測する
- JITコンパイラ分類
- レベル1
-
- shuJITなど
- 低い最適化
- コンパイル時間は短い・軽量
- レベル2
-
- Java 2 SDK (Reference Implementation)のsunwjitなど
- 中程度〜高度な最適化
- method inliningはおこなわない
- レベル3
-
- Java 2 SDK for Solaris (Production Release)のsunwjitなど
- 高度な最適化
- 静的解析による限られたmethod inlining
- レベル4
-
- IBMのJITコンパイラなど (HotSpot以外)
- 高度な最適化
- 実行履歴にもとづいたレシーバークラス予測によるmethod inlining
- Mixed-modeインタープリタ
- レベル5
-
- HotSpotコンパイラ
- 高度な最適化
- 実行履歴にもとづいたレシーバークラス予測によるmethod inlining
- Mixed-modeインタープリタ
- JCPにおける仕様策定作業との連動
- 仕様設計や実装時に,実際にHotSpotで高速なコードが生成されるかを検証している
- 今後導入される高速化のための新しい仕様に,すみやかに追従できる
- Servletシステムには,レベル3以上のJITコンパイラが望ましい
- レベル3以下のServletシステムの致命的なボトルネック部分には,オブジェクト指向プログラミング的には望ましくないが,以下のように対策できる
- ボトルネックとなるメソッドをfinal宣言 (有効かどうかはJITコンパイラに依存)
- ボトルネックとなるメソッドの人手によるインライン展開
- ボトルネックとなるクラス構成の簡略化
スレッド
- Javaスレッドの実現方法
- ユーザレベル・スレッド…Greenスレッド
- カーネルの変更が不要で実装が容易
- カーネル空間への切り替えがない分速い
- プロセス内でスケジューリングするので,マルチプロセッサに対応できない(= 同時に複数のCPUを使用できない)
- I/Oをうまく多重化できないことがある
- 標準入力でブロックすると,プログラム全体がブロックする
- プログラム内で明示的にスケジューリング可能なポイントを指定する必要がある
- カーネルスレッド…Win32スレッド
- カーネルの変更が必要
- カーネル空間への切り替えがある分遅い
- マルチプロセッサ対応 (同時に複数のCPUを使用できる)
- プログラム内で明示的にスケジューリング可能なポイントを指定しなくても,時分割されることが多い
- ユーザレベル・スレッドの2層構造…Solarisスレッド (ネイティブスレッド)
- カーネルの変更が必要.
- カーネルスレッドのパフォーマンスの問題を改善できる.
- 複雑なために実装が難しく,バグが残りやすい
- マルチプロセッサ対応 (同時に複数のCPUを使用できる)
- プログラム内で明示的にスケジューリング可能なポイントを指定しなくても,時分割されることが多い
- Solarisでは,オプションで切り替えが可能
- -green…Greenスレッド (サポートされていない場合もある)
- -native…ネイティブスレッド (native thread packのインストールが必要な場合もある)
- ユーザレベル・スレッドの使用は避ける
OutOfMemoryError
- servletシステムでは,OutOfMemoryErrorに対してロバストにしておくべきである.
- GCアルゴリズムによっては,ヒープ残量に余裕があってもOutOfMemoryErrorが発生する.
- システムによっては,必ずしも事前に最大ヒープ使用量を確定できずに実行時にOutOfMemoryErrorが発生することもあるが,これはリクエスト固有であり,致命的とは限らない
- 対策
- Servlet EngineがOutOfMemoryErrorで落ちないようにする.
- デッドロックしたスレッドの発生を防止するために,OutOfMemoryErrorが発生しても,wait()〜notify()の対応が正しくなることを保証する…notify()をfinally節で実行
巨大なファイルの扱い
- largefile awareness…2GB以上のファイルが正しく扱える
- 32bitでなく64bitで扱う必要がある
- 32bitで論理的にアドレスできるのは4GBだが,最上位ビットは標準ライブラリで負の帰り値でエラーを示すなどの目的で使用され,31bitだけが有効である.
- largefile未対応の場合の状況
- 2GBを越えるファイルの存在が検出できない
- 2GBを越えるファイルが読めない
- 2GBを越えるファイルが一覧に含まれない
- Java VMはlargefile awareであるとは限らない
- Java Core APIの一部が,long (64bit)でなくint (32bit)で表現
- 実装が必ずしも対応しているとは限らない
- Solarisでは標準APIは32bitなので,largefile対応は64bit用APIを使わねばならない
- Java 2 SDK 1.2 Reference Implementation…×
- Java 2 SDK 1.2 Production Release…○
- Java 2 SDK 1.3 Production Release…○ (ただしRC以降の版)
- Windowsでは,ファイルシステムによって異なる