レベル: 中級 Caroline Gough (goughc@uk.ibm.com), Software Engineer, IBM Andrew Hall (andhall@uk.ibm.com), Software Engineer, IBM Hursley Lab Helen Masters (helen_postlethwaite@uk.ibm.com), Software Engineer, IBM Hursley Lab Alan Stevens (alan_stevens@uk.ibm.com), Software Engineer, IBM Hursley Lab
2007年 6月 12日
リアルタイム Java™ を話題にした 6 回連載の第 5 回では、IBM® WebSphere® Real Time に用意されたツールを使ってリアルタイム Java アプリケーションを作成し、デプロイする方法を紹介します。サンプル・アプリケーションを例に、ガーベッジ・コレクションによる一時停止を制御する Metronome ガーベッジ・コレクター、ランタイム・コンパイルによる一時停止を回避するための AOT (Ahead-Of-Time) コンパイラー、そしてとりわけ厳しいタイミング要件に対応するための NoHeapRealtimeThread について説明します。
この連載ではこれまで、IBM WebSphere Real Time がガーベッジ・コレクション (GC) の不確定性の問題をごくわずかな時間にまで抑える方法を説明してきました。この方法は、Java プラットフォームがカバーする範囲とメリットを、以前は Ada などのリアルタイム (RT) 処理に特化したプログラミング言語がカバーしていた範囲にまで広げます。RT ハードウェアとオペレーティング・システムは大抵カスタマイズされていて、理解するのが難しかったりしますが、WebSphere Real Time はそれとは対照的に、IBM BladeCenter® LS20 (「参考文献」を参照) や同様のハードウェアと互換性を持つ Linux® の RT バージョンで動作します。WebSphere Real Time は、RT アプリケーションにとって一般的な以下の要件を満たします。
- 低遅延: 限られた時間内で確実に信号に応答すること
- 確定性: GC による不確定な一時停止がないこと
- 予測可能性: スレッドの優先度により実行順序が決定され、実行時間が一定であること
- 優先順位の逆転の抑止: 中間の優先度のスレッドが実行されていることが原因で、(高い優先度のスレッドが必要とするロックを保持している) 低い優先度のスレッドによって、高い優先度のスレッドがブロックされないこと
- 物理メモリーへのアクセス: デバイス・ドライバーなどの RT アプリケーションは多くの場合、物理メモリーにアクセスする必要があります
今回の記事では、WebSphere Real Time に用意されたツールを使って RT Java アプリケーションを作成してデプロイする方法を紹介し、RT の確定性を高くしてプログラムを実行する方法を説明しながら、連載の今までの記事を振り返っていきます (以前の記事を読むと参考になりますが、必須ではありません)。また、WebSphere Real Time 付属の Lunar Lander サンプル・アプリケーションを例に、Metronome などの RT GC ポリシーを利用して予測可能性を向上させる方法、そしてアプリケーションの AOT (Ahead-Of-Time) コンパイルを行って RT 環境での確定性を向上させる方法を説明します。最後に、ガーベッジ・コレクターによって制御されることがないメモリーを使って RT アプリケーションを設計および実装し、RT Java アプリケーションを最大限に活用するためのヒントと秘訣を探ります。
この記事で説明するプログラムを実行するには (独自の RT Java アプリケーションを作成すれば、さらに理想的です)、WebSphere Real Time がインストールされたシステムを用意する必要があります (この技術の入手方法についての詳細は、「参考文献」を参照してください)。
Metronome ガーベッジ・コレクターの利点
Metronome は WebSphere Real Time のガーベッジ・コレクターです。その利点は WebSphere Real Time付属のサンプル・アプリケーションを使い始めるとわかります。サンプル・アプリケーションは、WebSphere Real Time をインストールしたディレクトリーの下の /sdk/demo/realtime/sample_application.zip に配置されています。
このサンプル・アプリケーションは、無人月着陸船 (Lunar Lander) モジュールの制御技術をシミュレートします。安全に着陸させるには、Lander の以下のロケット・エンジンを正確にデプロイしなければなりません。
- 下降速度を減速させるための垂直エンジン
- 着地点に位置合わせするための水平エンジン
コントローラーはレーダー・パルスが戻ってくるまでの時間を基に Lander モジュールの位置を計算します。図 1 に、シミュレーションを示します。
図 1. Lunar Lander
例えば GC による一時停止などによって、信号が戻ってくる時間に少しでも遅延が生じると、Lander の位置は誤って計算されてしまいます。その理由は、レーダー・パルスが戻るまでの時間が長くなるということは距離が離れていっていることを意味するため、コントローラーは誤って推定された位置を基に調整を行ってしまうからです。これでは明らかに、Lander だけでなく、どのような RT システムにも悲惨な結果をもたらします。
標準 Java が RT アプリケーションを実行するには適していないことを示す 1 つの方法は、コントローラーがどれほど正確に Lander の正しい軌道を維持し、どれほど正常に Lander を着陸させられるかを測定することです。図 2 は、標準 Java VM を使用したコントローラーのシミュレーションをグラフにしたものです。赤い線が Lander の実際の位置を示し、青い線がレーダーによって測定された位置を示します。
図 2. 標準 Java VM を使用したコントローラーのシミュレーション
この飛行は最終的には着陸に成功しましたが、図 2 のグラフにはレーダーによる高度測定値 (青い線) が何度か急上昇している様子が示されています。この急上昇は、GC による一時停止に対応しています。何度か実行を繰り返すなかで、GC による一時停止によって、過剰な着陸速度 (垂直位置エラー) または着陸地点の誤差 (水平位置エラー) が引き起こす衝突事故の原因となるような、大きな位置測定値の誤差が生じています。この GC 実行時の不確定な動作が、これまで RT アプリケーションに標準 Java プラットフォームが使用されていない主な理由を示しています。
GC による一時停止の問題に対するさまざまなソリューションを提供しているのが、RTSJ (Real-Time Specification for Java) です。この仕様は Java プログラマーに自動メモリー管理の重要性を説いている一方で、GC の影響を避けるために、プログラマーがメモリーを管理しなければならない新たなメモリー領域を導入しています。
NoHeapRealtimeThreas
についてのセクションで説明するように、この新たなメモリー領域の導入は、信頼性の高い Java アプリケーションを作成するという難問をさらに難しくするので、非常に短い一時停止なら許容できる多くの RT アプリケーションには、代わりに WebSphere Real Time の Metronome のような RT ガーベッジ・コレクターを使用するほうが適しています。
Lunar Lander アプリケーションを Metronome で実行すると、Lander の実際の位置を遥かに正確に追跡するグラフになります。このグラフには高度測定値の顕著な急上昇はないため、毎回安全に着陸することができます (図 3 を参照)。
図 3. WebSphere Real Time を使用したコントローラーのシミュレーション
この 2 回目の実行では、コントローラーの Java コードは変更していません。これは RT ガーベッジ・コレクターのメリットを生かした通常の J2SE アプリケーションです。
サンプルの呼び出しに -verbose:gc パラメーターを追加すると、以下のような出力で減少した GC による一時停止の詳細を確かめられます。
<gc type="heartbeat" id="2" timestamp="Tue Apr 24 04:00:58 2007" intervalms="1002.940">
<summary quantumcount="171">
<quantum minms="0.006" meanms="0.470" maxms="0.656" />
<heap minfree="142311424" meanfree="171371274" maxfree="264060928" />
<immortal minfree="15964488" meanfree="15969577" maxfree="15969820" />
</summary>
</gc>
|
上記のサンプル出力では、デモ実行中の 1 秒間を抜粋して GC アクティビティーをレポートしています。この出力によると、GC の実行回数 (quantumcount) は 171 回で、アプリケーションがこのインクリメンタル方式の GC による一時停止で被った平均一時停止時間 (meanms) は 0.470 ミリ秒でした。
アプリケーションの実行と GC による一時停止が交互に発生している様子をさらに詳細に調べるには、Metronome トレース・ファイルを記録して、インターリーブの様子を TuningFork 分析ツール (「参考文献」を参照) を使って視覚化するという方法もあります。それが、図 4 です。
図 4. TuningFork で視覚化したデモの一部
GC による一時停止が最小限になると、今度は実行中のアプリケーションを不安定にする他の要因が際立ってきます。その 1 つが、JIT (Just-In-Time) コンパイラーのアクティビティーです。優れたパフォーマンスのためには Java バイトコードをネイティブ・コードにコンパイルすることが必須ですが、ネイティブ・コードを生成する際の動作には一時停止が伴います。この問題のソリューションとなるのが、AOT コンパイルによって Java バイトコードをプリコンパイルするという方法です。
アプリケーションに AOT コンパイルを適用する場合
通常、Java ランタイムは JIT コンパイラーを使用して、Java アプリケーション内で最も頻繁に実行されるメソッドを対象に動的にネイティブ・コードを生成します。RT環境のアプリケーションには、厳しい時間制限が課せられたアプリケーションはもとより、動的コンパイル・アクティビティーに伴う不確定性を許容できないアプリケーションがあります。それ以外のアプリケーションにとっても、複雑なアプリケーションの起動に使用する多くのメソッドをコンパイルする上でのオーバーヘッドは望ましくありません。このような問題に直面したアプリケーション開発者に役立つのが、AOT コンパイルです。
 | |
よくある過ちは、プリコンパイルされたコードは常にアプリケーションのパフォーマンスを改善すると思い込むことです。解釈されたコードとプリコンパイルされたコードを切り替えるにはコストがかかるため、必ずしもパフォーマンスの改善につながるとは限りません。アプリケーションの一部はプリコンパイルされていて、その他の部分はプリコンパイルされていなければ、AOT コンパイルを使用しなかった場合よりもアプリケーションの速度が遅くなることさえあります。このため、AOT コンパイルの対象は慎重に選ばなければなりません。
|
|
AOT コンパイルでは、アプリケーションの Java メソッドのネイティブ・コードが生成されてからアプリケーションが実行されます。そのため、ユーザーは動的コンパイルの不確定性を回避できると同時に、ネイティブ・コンパイルに関連したパフォーマンス上のメリットをそのまま最大限に利用することができます。ただし、AOT コンパイル済み (プリコンパイル済みとも呼ばれます) コードの実行は通常の場合、動的 JIT コンパイラーを使用した場合よりも多少時間がかかることを理解しておかなければなりません。JIT コンパイラーで動的に生成されるコードとは違い、プリコンパイルされたコードはその静的性質により、頻繁に使用されるメソッドが時間の経過に伴いさらに最適化されてもそのメリットを受けられないからです。WebSphere Real Time では現在、動的 JIT コンパイルとプリコンパイル済みコードの混在を許可していません。以上のことをまとめると、AOT コンパイルでは動的コンパイルが発生しないことから、実行中にパフォーマンスへ及ぼす影響が小さくなり、ランタイム・パフォーマンスの確定性が高くなると同時に、動的な問題を解決して Java への準拠を維持します。
JIT コンパイラーが最適化を実行するために使用する技術、JIT コンパイラーと AOT コンパイラーの長所と欠点、そしてこの 2 つのコンパイラーの比較についての詳細は、「リアルタイム Java、第 2 回: コンパイル技術を比較する」を読んでください。
プリコンパイル済みコードの生成
AOT コンパイル・ツール jxeinajar は、JAR または ZIP ファイル・フォーマットで保管されたクラスからネイティブ・コードを生成します。このツールでは、AOT コンパイル済みコードを JAR ファイルの各クラスにあるすべてのメソッドに対して作成することも、選択したメソッドに対して作成することもできます。AOT コンパイル済みコードは、JITコンパイラーが最適化レベルを固定して生成した場合のネイティブ・コードに相当します。生成されたコードは JXE (Java eXEcutable) として知られる内部フォーマットで保管されます。jxeinajar ツールは JXE ファイルを JAR ファイルにラップして、WebSphere Real Time が実行できるようにします。
 |
RT Java ランタイム環境の実行は、-Xrealtime オプションで指定します。-Xrealtime オプションが省略されていると、jxeinajar ツールは機能しません。このオプションが指定されていない場合に起動されるのは、標準 IBM SDK and Runtime Environment for Linux Platforms, Java 2 Technology バージョン 5.0 です。 |
|
AOT コンパイルには 2 段階のプロセスがあります。第 1 段階は AOT コードの生成 (jxeinajar ツールを使用) で、AOT コンパイラーによってネイティブ・コードが作成されます。第 2 段階では、このコードが Java ランタイム環境 (JRE) 内で実行されます。
以下は、現行ディレクトリー内のすべての JAR または ZIP ファイルに対して AOT コンパイル済みコードを作成するコマンドです (aotJarPath は、プリコンパイル・ファイルを作成するディレクトリー)。このコマンドは、$JAVA_HOME/bin が $PATHにあることを前提とします。
jxeinajar -Xrealtime -outPath aotJarPath
|
上記のコマンドを実行すると、以下の内容が出力されます。
J9 Java(TM) jxeinajar 2.0
Licensed Materials - Property of IBM
(c) Copyright IBM Corp. 1991, 2006 All Rights Reserved
IBM is a registered trademark of IBM Corp.
Java and all Java-based marks and logos are trademarks or registered
trademarks of Sun Microsystems, Inc.
Searching for .jar files to convert
Found /home/rtjaxxon/demo.jar
Searching for .zip files to convert
Converting files
Converting /home/rtjaxxon/demo.jar into /home/rtjaxxon/aot///demo.jar
JVMJ2JX002I Precompiled 3098 of 3106 method(s) for target ia32-linux.
Succeeded to JXE jar file /home/rtjaxxon/demo.jar
Processing complete
Return code of 0 from jxeinajar
|
作成される JAR ファイルは本物の JAR ではなく、クラス・ファイルは含まれていません。このファイルに含まれているのは、ネイティブ・コードにアクセスするために使用される、すべてのクラスおよびプレースホルダー・クラス・ファイルの JXE ファイルです。これらの JXE ファイルは WebSphere Real Time バージョンに固有のファイルで、他の Java ランタイム・ツールでは使用できません。
デフォルトの動作を無効にするには、コマンド・ラインに個別の JAR または ZIP ファイルを指定します。コマンドに -recurse オプションを追加すると、入力ファイルの検索を拡張してサブディレクトリーを含めることができます。
プリコンパイル済みファイルの識別
jxeinajar ツールが作成するファイル・フォーマットには、JXE ファイル、そして JXE ファイル内の個々のクラス・ファイルへの事実上のポインターが含まれます。JAR または ZIP ファイルの中身を一覧表示すると、そのファイルが jxeinajar ツールによって生成されたものかどうかを一目で判断できます。例えば demo.jar を調べる場合、ファイルの中身を一覧表示するコマンドは以下のようになります。
JAR ファイルが jxeinajar ツールによって生成された場合、出力は以下のようになります。
0 Thu Apr 19 13:59:14 CDT 2006 META-INF/
71 Thu Apr 19 13:59:14 CDT 2006 META-INF/MANIFEST.MF
68 Thu Apr 19 13:59:14 CDT 2006 demo.class
4119 Thu Apr 19 13:59:14 CDT 2006 jxe22A6B69D-010D-1000-8001-810D22A6B69D.class
|
JAR ファイルに含まれる追加の JXE ファイルが、その JAR ファイルを jxeinajar ツールによって生成された JAR ファイルとして特定します。そうでない場合、出力は以下のようになります。
0 Thu Apr 19 09:00:01 CDT 2006 META-INF/
71 Thu Apr 19 09:00:01 CDT 2006 META-INF/MANIFEST.MF
846 Thu Apr 19 09:00:01 CDT 2006 demo.class
|
プリコンパイル済みコードの実行
AOT コンパイルが完了したアプリケーションを実行するには、以下のコマンドを使用します。
java -Xrealtime -Xnojit -classpath aotJarPath AppName
|
 | | プリコンパイルしたアプリケーション JAR ファイルがクラスパスの最初に記載されていることを必ず確認してください。そうでないと、プリコンパイル済みコードが実行されません。 |
|
WebSphere Real Time を使用する場合、動的 JIT コンパイルと AOT コンパイルは混在させられないことを覚えておいてください。-Xnojit オプションを省略すると、Java VML に有効な AOT コンパイル済みコードは一切使用されません。コードは解釈されるか、あるいは JIT によって動的にコンパイルされる結果となります。RT Java VM を有効にするには、コマンドに -Xrealtime オプションを指定します。このオプションが指定されない場合、WebSphere Real Time に付属の SE Java VM が代わりに使用されます
-Xnojit フラグをセットすると、WebSphere Real Time はインタープリターを使用して、プリコンパイルされていないすべてのメソッドを実行します。つまり、プリコンパイル済み JAR ファイル内、あるいはクラスパスに指定されたその他の JAR ファイルにアプリケーション・コードのプリコンパイルされていないバージョンが最初に見つかると、そのコードは解釈された速度でのみ実行されることになります。
システム JAR の AOT コンパイル
アプリケーションだけをプリコンパイルするのではなく、主要なシステム JAR ファイルに対しても AOT コンパイルを行うようにしてください。標準 Java API を使用するアプリケーションは、システム JAR ファイルも同じくコンパイルされていない限り、事実上部分的にしかコンパイルされません。標準 API クラスの大多数は core.jar ファイルと vm.jar ファイルに保管されているので、まずはこの 2 つのファイルの AOT コンパイルを実行することをお勧めします。RT アプリケーションの場合はrealtime.jar もプリコンパイルする必要があります。さらにパフォーマンスを向上させるために他のどのシステム・ファイルをプリコンパイルすればいいかは、アプリケーションの性質によって決まります。
システム JAR ファイルの AOT コンパイル・プロセスは、他のあらゆる JAR ファイルの場合と同じです。ただし、システム JAR ファイルはブート・クラスパスからロードされるので、プリコンパイルされたすべてのシステム JAR ファイルをブート・クラスパスの先頭に追加して、これらのファイルが確実に使用されるようにする必要があります。それには、以下のコマンドを使用します。
java -Xrealtime -Xnojit
-Xbootclasspath/p:aotSystemJarPath/core.jar:aotSystemJarPath/vm.jar:
aotSystemJarPath/realtime.jar -classpath aotJarPath/realTimeApp.jar realTimeApp |
-Xbootclasspath/p: オプションの /p によって、プリコンパイルされたシステム JAR ファイルがブート・クラスパスの先頭に追加されます。ブート・クラスパスを操作するには、-Xbootclasspath/p: オプションと -Xbootclasspath/a: オプション (それぞれ設定と追加を行うためのオプション) も使用できますが、-Xbootclasspath: または -Xbootclasspath/a: を使用して AOT コンパイル済み JAR ファイルをブート・クラスパスに組み込んだ場合、コンパイルされたクラスは使用されません。
選択したプリコンパイル済み JAR の確認
クラスパスでは間違いを犯しがちです。特にアプリケーションが複数の JAR ファイルで構成されていて、システム JAR ファイルも同じくプリコンパイルする場合はなおさらです。クラスパスに誤りがあると、本来実行されるべきプリコンパイル済みコードではなく、プリコンパイルされていないコードが実行される結果となってしまいます。使用するクラスがプリコンパイルされていることを確実にするには、以下のオプションを組み合わせて使用してください。
-
-verbose:relocations は、プリコンパイルされたコードの再配置情報を STDERRに出力します。プリコンパイル済みメソッドが実行されるたびに、ログ・メッセージが出力されます。以下は、このオプションの出力例です。
Relocation: realTimeApp.main([Ljava/lang/String;)V <B7F42A30-B7F42B28> Time: 10 usec |
-
-verbose:class
は、各クラスがロードされるときに STDERRにメッセージを書き込みます。以下は、このオプションの出力例です。
class load: java/lang/Object
class load: java/lang/J9VMInternals
class load: java/io/Serializable
class load: java/lang/reflect/GenericDeclaration
class load: java/lang/reflect/Type
class load: java/lang/reflect/AnnotatedElement |
-
-verbose:dynload は、Java VM によってロードされたクラスごとの詳細情報を提供します。情報には、クラス名、クラスのパッケージ、クラス・ファイルの場所などが含まれます。以下は、このオプションによる詳細情報のフォーマット例です
<Loaded java/lang/String from /myjdk/sdk/jre/lib/vm.jar>
<Class size 17258; ROM size 21080; debug size 0>
<Read time 27368 usec; Load time 782 usec; Translate time 927 usec> |
このオプションではプリコンパイルされた JAR ファイルのクラスは表示されませんが、-verbose:class オプションと組み合わせると、クラスの有無によってどのクラスがプリコンパイルされているかを推測できます。-verbose:class の出力に表示されていて、 -verbose:dynloadの出力には表示されていないクラスが、プリコンパイル済み JAR ファイルからロードされたクラスだと判断できます。この場合に必要となる詳細オプションは、-verbose:class,dynload. です。
プロファイルを指定した AOT コンパイル
アプリケーションで頻繁に使用するメソッドのプロファイルを作成し、これらのメソッドに対してのみ AOT コンパイルを行うことで、さらに最適化されたプリコンパイル済み JAR ファイルのセットを作成することができます。
このプロファイルを動的に作成するには、-Xjit:verbose={precompile},vlog=optFileName オプション (optFileName は、プリコンパイル対象のメソッドをリストしたファイルの名前) を指定してアプリケーションを実行します。
java -Xjit:verbose={precompile},vlog=optFileName -classpath appJarPath realTimeApp
|
このオプションによって追加で生成されるファイルには、JIT コンパイラーがアプリケーションの実行中にコンパイルしたメソッドに対応するメソッド・シグニチャーのリストが含まれます。このファイルは必要に応じてテキスト・エディターで簡単に編集することができます。このファイルを jxeinajar ツールに提供すれば、プリコンパイルするメソッドを制御することができます。ファイルをツールに提供するには、以下のコマンドを実行します。
jxeinajar -Xrealtime -outPath aotJarPath-optFile optFileName
|
プロファイル指定のAOT コンパイルについては、WebSphere Real Time 付属の InfoCenter にも説明が記載されています (オンライン InfoCenter へのリンクは、「参考文献」を参照)。InfoCenter では、前のセクションで説明した Lunar Lander のランタイム・プロファイルを生成する方法から、このプロファイルを使用して Lunar Lander アプリケーションとシステム JARファイルを選択的にプリコンパイルする方法まで案内しています。別のアプリケーションのプリコンパイルを試してみたいという方は、次のセクションで説明する Sweet Factory アプリケーションを使用することもできます。
NoHeapRealtimeThread を使用する場合
WebSphere Real Time には RTSJ の完全な実装が含まれています。RTSJ は Metronome などの RT ガーベッジ・コレクターが登場する以前に設計されたもので、Java ランタイムから予測可能な低遅延パフォーマンスを引き出すための代替手段が用意されています。
RTSJ が作成された当時、Java ランタイムで予測可能な実行を妨げていた 2 大要素は、JIT コンパイラーとガーベッジ・コレクターでした。この 2 つの技術がそれぞれに使うプロセッサー時間は、アプリケーション・プログラマーには管理できません。どちらの技術もその動的な性質により、予測不可能な遅延を Java アプリケーションにもたらします。場合によっては、大抵の RT システムでは許容できない数秒間の遅延が発生することもあります。
JIT コンパイラーの場合は単純にオフにしたり、あるいは AOT コンパイルなどといった別の技術に置き換えることもできますが、GC はそう簡単には無効にすることができません。GC を削除するには、まずその前に代わりとなるメモリー管理ソリューションを用意する必要があります。
標準ガーベッジ・コレクターによって引き起こされる遅延を許容できない RT システムをサポートするため、RTSJ では標準 Java ヒープを補完する永続メモリー領域およびスコープ・メモリー領域を定義しています。さらに RTSJ は 2 つの新規スレッド・クラス、RealtimeThread と NoHeapRealtimeThread (NHRT) のサポートを追加して、ヒープ以外のメモリー領域の使用を含めた他の RT 機能もアプリケーション・プログラマーが利用できるようにしています。
NHRT は、Java ヒープで作成されたオブジェクトを扱うことができないスレッドなので、ガーベッジ・コレクターとは独立して動作することができ、遅延も少なく予測可能な状態で実行することができます。NHRT は、スコープ・メモリーまたは永続メモリーを使用して独自のオブジェクトを作成します。そこで必要となるのが、ヒープをベースとした標準の Java プログラミングとは大幅に異なるプログラミング・スタイルです。
それでは、ヒープ以外のメモリーの使用に伴う独特のプログラミング上の課題を説明するため、ここからは NHRT を使用する単純なアプリケーションを開発してみます。
サンプル・シナリオ
これから実装するのは、キャンディー工場のオートメーション・システムです。工場内には複数の生産ラインがあり、原材料からさまざまな種類のキャンディーを製造して瓶に詰めます。実装するオートメーション・システムの目的は、詰められたキャンディーが多すぎたり、少なすぎたりする瓶を検出し、内容量が適切でない瓶を回収するよう作業員に知らせることです。
瓶にキャンディーが詰められたら、瓶の重量を測定して各瓶に詰められたキャンディーの数をチェックします。キャンディーの数が目標の 2 パーセント以内に収まっていない場合、メッセージを作業員の制御画面に送信して問題を通知します。作業員は制御パネルに表示された瓶の ID を基に、該当する瓶を見つけて梱包キューから取り除き、制御パネルで瓶が除去されていることを確認します。それぞれの瓶の重量は監査のためにログ・ファイルに書き込まれます。
図 5 に、サンプル・シナリオを図解します。
図 5. キャンディー工場のシナリオ
このサンプルはもちろん多少不自然なものですが、NHRT アプリケーションを作成する際の、特に NHRT とその他のスレッド・タイプとのデータ共有に関する難問を検討するには役立ちます。
外部インターフェース
このシステムが扱わなければならない外部エンティティー・クラスは、生産ラインの計量器、作業員のコンソール、そして監査ログの 3 つです。生産ラインと作業員のコンソールは、システムに用意された Java インターフェースにすでにカプセル化されています。
計量器とのインターフェースには、weighJarGrams() という 1 つのメソッドがあります。このメソッドは、次の瓶が計量器に渡されるまでブロックし、瓶の重量をグラム単位で返します。瓶の到着間隔は可変ですが、生産率を最大限にするために間隔を 10 ミリ秒まで小さくすることができます。ただし weighJarGrams()メソッドが頻繁にポーリングされなければ、瓶の測定ミスが発生します。
計量器は生産ラインのコンポーネントで、生産中のキャンディーの種類と充填中の瓶のサイズを問い合わせるメソッドがあります。
作業員のコンソールには jarOverfilled() と jarUnderfilled() という 2 つのメソッドがあり、どちらも瓶の ID を引数に取ります。これらのメソッドは作業員がメッセージを確認するまでブロックします (数秒かかる場合があります)。
実装する MonitoringSystem インターフェースには、startMonitoring() と stopMonitoring() という 2 つのメソッドがあります。startMonitoring() メソッドが引数に取るのは ProductionLine オブジェクトと WorkerConsole オブジェクトで、この 2 つのオブジェクトは通信する際の引数として必要になります。
T監査ログは、audit.log という名前のフラット・ファイルとして指定します。このファイルの各行は、コンマ区切りのストリングで、タイムスタンプ (timestamp), 瓶の ID (jar id), キャンディーの種類を表すコード (sweet type code), 瓶のサイズを表すコード (jar size code), 瓶の重量 (mass of jar) というフォーマットになります。
図 6 の UML クラス図に、上記のインターフェースを示します。
図 6. インターフェースの UML クラス図
ソリューションの設計
仕様がわかったところで、今度はソリューションを設計する番です。問題は 2 つの部分に分けられます。第 1 の問題は、生産ラインをポーリングして瓶の重量をチェックすること、そして第 2 の問題は監査ログを作成することです。
生産ラインのポーリング
WeighingMachine インターフェースに関しては、weighJar() メソッドを頻繁にポーリングする必要があるため、ProductionLine ごとに専用のスレッドを用意して設計をスケーラブルにするのが賢明です。そこで、NHRT を使用してポーリング・スレッドがガーベッジ・コレクションに割り込まれて測定し損なう可能性を最小限にします。
瓶を計量した後に必要となるのは、重量に相当するキャンディーの数を計算し、その数を目標値と比較することです。測定にその後必要な処理量を予測するのは容易ではありません。瓶内のキャンディーの数が許容範囲外であれば WorkerConsole と通信しなければなりませんが、それには数秒かかる場合があるからです。
瓶は 10 ミリ秒間隔で運ばれてくる可能性もあるので、ポーリング・スレッドで処理量を計算するのは不可能であることは明らかです。そのため測定値は別の計算スレッドに渡します。処理によっては時間がかかるため、最新の測定値を処理するスレッドを常に使用できる状態にするには、生産ラインごとに複数の処理スレッドが必要となります。
生成されたデータごとに新しいスレッドを作成するという方法もありますが、それではスレッドの開始と停止で大量のプロセッサー時間を無駄に消費してしまいます。そこで、CPU 時間を有効に使用するためにデータを処理する NHRT のプールを作成します。実行中のスレッドのプールを維持することで、スレッドを実行する際の開始とシャットダウンのオーバーヘッドをなくします。
1 つのスレッド・プールをすべての生産ラインに共有させることは可能ですが、そうなると、複数のスレッドがアクセスするデータ構造を同期させなければなりません。また、スレッド・プールを 1 つにすると、重大なロック競合が発生するおそれがあります。ソリューションをスケーラブルにするため、生産ラインにはそれぞれに固有の小規模なスレッド・プールを追加することにします。
スレッド・プールの設計には、プールのサイズ設定や管理手法など、この記事の範囲外の多くの検討事項が関わってきますが、この記事では説明のため、ProductionLine オブジェクトごとに 10 のスレッド・プールを作成し、何らかの理由でスレッドがなくなったときにはプールを拡張するという前提にします。
監査ログの作成
システムの他のコンポーネントとは異なり、監査ロギング・コンポーネントには厳しい時間制約はありません。コンピューターがクラッシュしたり、電源が切られる可能性を (単純に) 無視すれば、唯一検討しなければならない重要な点は、ある時点で測定値がログに記録されるようにすることだけです。
この点を考え、ログ・ファイルへの出力には単一の java.lang.Thread を使うことにします。ログ・ファイルへの出力作業が行われるのは、NHRT が追加の作業を待機中で、ガーベッジ・コレクターがアクティブになっていないときです。従来のヒープ・ベースの Java 環境と NHRT のヒープ以外の環境との間には 1 つのインターフェースしか導入していないことから、この設計上の決定の裏にはさまざまな意味が隠されています。後で説明しますが、このインターフェースを扱う際には注意が必要です。
図 7. は、このアーキテクチャーの概要です。
図 7. アーキテクチャーの概要図
 |
単一親 (single-parent) ルール
スコープの共有に基づいてスレッド間でデータを共有するためのアーキテクチャーを設計することは可能ですが、そのようなアーキテクチャーを作成するのは容易なことではなく、多くの場合は直観に反したものになります。その第 1 の理由は、単一親ルールがあるからです。このルールでは、スコープには固有の 1 つの親しか許容されません。
単一親ルールが必要となる理由は、RT スレッドが複数のメモリー領域に次々と入り込むと、メモリー領域のスタックが蓄積されてしまうことを考えれば理解できます。このメモリー領域の一部がスコープである場合、一部のメモリーはすぐに収集され、一部のオブジェクト (永続メモリー内のオブジェクト) は決して収集されることがないという事態になります。
RTSJ では、オブジェクトが予期しないタイミングで解放されて謎の消失を遂げるようなアーキテクチャーをアプリケーション・プログラマーに作成させないためのルールを定義する必要がありました。しかし定義されたルールは理解しにくく、プログラムするのも厄介です。単一親ルールは、RT Java プログラマーが懸念しなければならない唯一のメモリー・アクセス・ルールであるというよりは、スレッド間のスコープ共有を困難にするルールです。
スコープ・メモリーには、親スコープという概念があります。スコープを使用する場合、最後に使ったスコープでまだ解放されていないスコープ (現行スレッドのメモリー領域スタックでの 1 つ下のスコープ) が、現在使われている他のスコープの親になります。あるスレッドのメモリー領域スタックの最初のスコープの場合は、基本スコープがそのスコープの親となります。基本スコープとは論理スコープのことで、そのなかでは実際にオブジェクトを作成することができません。スコープが使用されていなければ、スコープには親がないことになります。
単一の親がスレッド間のスコープ共有を困難にする理由は、複数のスレッド全体でメモリー領域に入るシーケンスを慎重に管理せざるを得なくなるからです。実質的には、スレッドごとのデータ (スレッドのメモリー領域スタック) をスレッドの共有データ (スコープの親フィールド) にアラインさせようとしているとも言えます。この場合に最も明らかな不正操作の例となるのは、異なるスコープを初期メモリー領域とした 2 つのスレッドを開始し、それぞれのスレッドを共有スコープに入れてデータを交換させようとすることです。この操作は単一のスコープに 2 つの親を持たせることになるので許可されません。
スレッドのメモリー領域スタックは、実際にはカクタス・スタック (cactus stack) です。そのため、メモリー領域を出ないでスタックをバックトラックし、下位に分岐を作成することもできます。複雑なメモリー領域スタックの構造を作成して管理するのは困難なことなので、もっともな理由がない限り避けるべきですが、メモリー領域スタックを慎重に使用すれば、大抵はスコープの共有が可能になります。
2 つのスレッド間でスコープを共有するのに最も簡単な方法は、以下のとおりです。
- 共有スコープを初期メモリー領域として両方のスレッドを開始します。この場合、スコープの親は基本スコープとなります。
- 永続メモリーで両方のスレッドを開始し、そこからスコープに入ります。それぞれのスレッドにとってこれが最初に入るスコープとなるのでスコープの親は基本スコープとなり、単一親ルールに違反したことにはなりません。
|
|
これで、NHRT アプリケーションに必要な作業内容を大体把握してもらえたと思うので、次の課題として、システムがどのメモリー領域で作業を行うことになるのかを考えてみましょう。
RT Java でのヒープ以外のメモリー
スコープ・メモリーと永続メモリーを設計に適用するには、それぞれの仕組みについてもう少し理解しておく必要があります。
永続メモリーで作成されるオブジェクトは決してクリーンアップされることがなく、アプリケーションの存続期間を通して維持されます。使い終わったオブジェクトでもスペースをそのまま占有し続けるので、そのスペースを再利用することはできません。これは当然、永続メモリーで作成するオブジェクトをすべて追跡し、時間の経過とともに作成されるオブジェクトの数が増えないようにするという責任をプログラマーに課します。永続メモリーのメモリー・リークは、RT Java アプリケーションではよくあるエラー原因です。
スコープ・メモリーで作成されるオブジェクトは、それが作成されたスコープの存続期間中、維持されます。スコープ・メモリーの各領域には基準カウントがあり、スレッドがスコープ・メモリーの領域に入ると基準カウントがインクリメントされ、スレッドが出て行くとデクリメントされます。基準カウントがゼロになると、スコープ内のオブジェクトが解放されます。スコープ・メモリー領域の作成時に指定される最大サイズは、その領域を用いるタスクに合わせて調整する必要があります。RT アプリケーションの設計者は通常、効率的にサイズを調整できるようにスコープを特定のタスクに関連付けます。スコープには固定サイズを事前に宣言しなければならないため、スコープは使用するメモリー量が予測不可能なタスクには向いていません。
Sweet Factory デモのメモリー・アーキテクチャー
ヒープ以外のメモリーについて多少理解できたところで、上記で設計したシステムに適用してみます。
メモリーの観点からすると、監査システムは問題になりません。監査システムは、ヒープ・メモリー内の java.lang.Thread で実行されます。使用するのが標準 Javaスレッドまたはヒープ・ベースの RT スレッドのどちらであれ、ストリング操作と入出力は意外なほど大量のメモリーを消費する場合があるので、これらの操作はガーベッジ・コレクターが管理するメモリー内で行うのが賢明です。
システム内の残りのスレッドは NHRT なので、定義上、オブジェクトの割り当てに Java ヒープを使うことはできません。選択できるのは、スコープ・メモリーと永続メモリーの組み合わせに限られます。
すべてのスレッドには初期メモリー領域があり、この領域はスレッドの存続期間中、使用されます。サンプルの設計では NHRT が長時間実行されることになるので、初期メモリー領域に何を選択するのであれ、初期起動の後にそのメモリー領域を使い続けることはできません。スコープ・メモリーまたは永続メモリーのどちらを使用するにしても、メモリーがクリーンアップされることはないので最終的には使い果されてしまいます。
現行のメモリー領域を消費するのはオブジェクトの割り当てだけなので、メモリー管理には、一定数のオブジェクトのみを使用するか、あるいはオブジェクトをまったく使用しないという方法が考えられます。スタックでプリミティブ値を使用すれば、現行のメモリー領域を使わずに作業を行えます (このスタックは、メソッドで使用される関数引数とフィールドを保持するメモリーのセクションです。このセクションは Java ヒープ・メモリーや永続メモリーまたはスコープ・メモリーからは分離されますが、オブジェクトを保持することはできません。保持できるのは、プリミティブ値またはオブジェクト参照だけです)。
ただし、Java 言語とそのクラス・ライブラリーが奨励するのは、オブジェクトを使って目標を達成することです。そこでこのサンプルでは、NHRT が実行する必要のある操作の場合はオブジェクトを作成し、その操作を実行するごとにメモリーを使用するということにします。
長時間、ただし期間は未定でシステムがフラット・メモリー・プロファイルを持つ必要があり、しかもオブジェクトを作成しなければならないというこのシナリオで最善の手法となるのは、スレッドを永続メモリーで開始し、スコープ・メモリーの領域を特定の限られたタスクに割り当てることです。
動作中のスレッドは常に、タスクの実行時にスコープ (そのタスクに応じてサイズが調整済み) に入ってタスクを実行し、それが終わるとスコープを出て消費したメモリーを解放します。この手法を強固にするには、実行するタスクを限定して、必要なスコープ・メモリーの量を予測して調整できるようにする必要があります。
複数のスレッドでスコープを共有するのは可能ですが、メモリー・スコープには単一の親しか許可されないというルールがあるため、そう簡単にはいきません (「単一親ルール」を参照)。共有スコープの管理が困難なのは、すべてのスレッドが出て行くまではスコープを再利用できないためです。これはつまり、複数のスレッドが同時にタスクを実行できるように、スコープのサイズを調整しなければならないことを意味します。
一般的に、1 つのスレッドで実行される 1 つのタスクが同時に使用できるスコープを 1 つに制限すれば、NHRT での開発は容易になります。サンプルでは、各生産ラインのポーリング・スレッドは、永続メモリーで開始し、前もって作成されたスコープを持ち、そのスコープには ProductionLine に問い合わせを行う前に毎回入るようにします。選別作業を行うそれぞれのプール・スレッドは永続メモリーで開始し、スタックでプリミティブ・データを使ってその計算を行います。スレッドごとに、WorkerConsole インターフェースを使用する必要がある場合に入るスコープも用意します (ここでオブジェクトが作成されることになります)。
スレッド間の通信
最後のメモリー問題は、スレッド間の通信方法です。ProductionLine ポーリング・スレッドがデータを選別プールに送信し、選別プールのそれぞれのスレッドがデータを監査スレッドに渡す必要があります。
この問題を解決する方法として考えられるのは、メソッドには単純にプリミティブ値を引数として渡すことです。データのすべてがスタック上にあれば、メモリー領域での問題がなくなります。
サンプル・アプリケーションをより面白いものにするため、Measurementクラスを作成することにします。このクラスのオブジェクトを使って測定データを渡すのですが、問題はどのメモリー領域でこれらのオブジェクトを作成するかです。Java ヒープは、NHRT がアクセスできないため使用できません。また、このアーキテクチャーではどのスコープもスレッド間で共有されないので、スコープ・メモリーも使用できません。
ヒープとスコープを候補から外すと、残るのは永続メモリーです。永続メモリーは回収されることがないため、思いのままに Measurementオブジェクトを作成し続けることはできません。それではメモリー不足になってしまいます。この問題を解決する答えは、永続メモリーで限られた数の Measurementオブジェクトを作成し、それを再利用することです。事実上、オブジェクトのプールを作成するということになります。
MeasurementManager による測定オブジェクトのプール
この例で作成する MeasurementManager クラスが持つのは、再利用可能な Measurement インスタンスを取得して返す静的メソッドです。Java SE プログラマーであれば、既存の LinkedList または Queue クラスを使って測定値を保持するためのデータ・ストアを用意しようと思うかもしれません。ただし、この方法は 2 つの理由で上手くいきません。第 1 の理由は、SE 収集クラスの大部分は裏でデータ構造を維持するためのオブジェクト (リンク・リストのノードなど) を作成しますが、このようにオブジェクトを作成すると永続メモリーのリークが発生してしまうからです。第 2 の理由はそれよりも難解です。この方法では、ヒープとヒープ以外のコンテキストで実行するスレッドを橋渡しするために、ほとんどのマルチスレッド・アプリケーションの場合と同様に、ロックによって使用するデータ構造への排他的アクセスを保証しようとします。しかし NHRT とヒープ・ベースのスレッド間でロックを共有すると、優先順位の逆転を保護する副次作用として、ガーベッジ・コレクターによる NHRT のプリエンプトが発生します。ガーベッジ・コレクターが NHRT に割り込む可能性を作ってしまうと、ヒープ以外のメモリーを使用するメリットがまるでなくなってしまいます。言うまでもなく、NHRT とヒープ・ベースのスレッドでロックを共有することは避けなければなりません。この問題の詳細は、「リアルタイム Java、第 3 回: スレッド化と同期」を参照してください。
RTSJ が提供している NHRT とヒープ・ベースのスレッド間のデータ共有ソリューションは、WaitFreeQueue クラスです。これらのクラスは、NHRT がデータの読み取りまたは書き込み (クラスに依存) を要求できる wait のない側を持つキューで、ブロックの危険性はありません。従来の Java 同期を使用するキューのもう一方の側は、ヒープ・スレッドが使用します。ヒープ以外のメモリーをベースとする環境とヒープ・メモリーをベースとする環境でのロック共有を避けることで、安全なデータ交換が可能になります。
サンプルでの MeasurementManager は、NHRT が測定値をフェッチするために使用され、またヒープ・ベースの監査スレッドが測定値を返すために使用されます。そのため、このインターフェースは WaitFreeReadQueue を使って管理します。WaitFreeQueue の wait のない側は、単一スレッドになるように設計されています。WaitFreeReadQueue が対象としているのは、書き出しプログラムが複数で読み取りプログラムが 1 つのアプリケーションです。サンプルで使用するのは複数の読み取りプログラムと 1 つの書き出しプログラムを持つアプリケーションなので、独自の同期を追加して一度に 1 つの NHRT だけが測定値を要求するようにします。同期をさらに追加することで WaitFreeQueue の用途を台無しにしているように聞こるかもしれませんが、 read() メソッドへのアクセスを制御するモニターは NHRT 間でしか共有されないため、ヒープとヒープ以外のコンテキストでロックが共有される危険はありません。
この説明が明らかにしているのが、NHRT アプリケーションを開発する際のもう 1 つの難題です。それは、NHRT アプリケーションでは Java コードの既存の部分をヒープ以外の環境で再利用するのがさらに困難だということです。今まで説明してきたように、それぞれのオブジェクトをどこから割り当てるか、そしてメモリー・リークをどのように回避するかについては慎重に考えざるを得ません。ヒープ以外のコンテキストでは、Java 言語とオブジェクト指向プログラミングの主要な強みの 1 つ、実装詳細のカプセル化が弱点になります。なぜならこのコンテキストでは、メモリー使用率を予測して管理することができないためです。
メモリー・モデルの設計はこれで完了です。メモリー領域を色分けした更新後のシステムを8 に記載します。
図 8. メモリー領域を色分けしたアーキテクチャーの概要図
スレッドの優先度
WebSphere Real Time を開発するときには、適切なスレッドの優先度を選択することが標準 Java コードでの場合より遥かに重要になってきます。選択が適切でないと、ガーベッジ・コレクターが NHRT をプリエンプトしたり、システムの一部で CPU が不足するおそれがあるからです。
スレッドの優先度についての詳細は、「リアルタイム Java、第 3 回: スレッド化と同期」を参照してください。サンプル・システムでは、以下を目標として優先度を設定します。
- ポーリング・スレッドに最大の優先度を指定して、測定ミスのリスクを最小限にする
- 選別プール・スレッドがガーベッジ・コレクターに割り込まれないようにする
上記の目標を達成するには、ポーリング・スレッドのスレッド優先度を 38 (最高位の RT 優先度) に設定し、選別プール・スレッドの優先度を 37 に設定します。監査スレッドは標準優先度 (5) を持つ標準 Java SE スレッドなので、NHRT より優先度が大幅に低くなります。
この構成は、ガーベッジ・コレクターのスレッドの優先度が監査スレッドよりわずかに上回り、NHRT より遥かに下回ることを意味します。
ブートストラップに関する検討事項: アプリケーションの起動
今まではアプリケーションの定常状態、つまり実行中になってからの動作について取り上げてきましたが、そもそもどのように起動するかについてはまだ検討していません。WebSphere Real Time アプリケーションの起動は標準 Java アプリケーションと同じように、Java ヒープの java.lang.Thread で実行されます。この Java ヒープから、異なるメモリー領域で複数のスレッド・タイプを開始しなければなりません。.
サンプル・アプリケーションでは、ブートストラップは一貫して MonitoringSystemImpl クラスの startMonitoring() メソッドで行います。このメソッドが、ヒープ・メモリーで実行する java.lang.Thread によって呼び出されるという前提です。
サンプル・アプリケーションでのブートストラップ・タスクには、以下のものがあります。
- 永続メモリーで 1 つ以上のポーリング・スレッドを作成する
- 永続メモリーで 1 つ以上のスレッド・プール・オブジェクトを作成し、それぞれにプールするスレッドを作成して永続メモリーで実行する
- 永続メモリーで監査スレッド・オブジェクトを作成し、ヒープ・メモリーで実行する
永続メモリーのオブジェクトは、 java.lang.Thread から ImmortalMemory.newInstance() メソッドを使って作成することもできます。この方法はコンストラクター引数がほとんどないクラスや、同じクラスの多数のオブジェクトを作成する場合には可能ですが、引数がたくさんあるコンストラクターを持つクラスの場合には、このようにオブジェクトを作成すると永続メモリーがすぐに乱雑になってしまいます。
RealtimeThreads は java.lang.Thread とは異なり、( Runnable を ImmortalMemory.enter()) に実装するオブジェクトを用意するか、) 永続メモリーをスレッドの初期メモリー領域として使えるようにすることによって、永続メモリーに入って作業を行うことが可能です。この方法の利点は、標準 Java コードを作成してすべての新規操作が永続メモリーにオブジェクトを作成するようにできることです。その一方で、ヒープ・ベースの java.lang.Thread から永続メモリーで実行する RT スレッドに接続するコードの見栄えが乱雑になるのを避けられないという欠点もあります。
サンプル・コードでは、MemoryArea オブジェクトと Runnable オブジェクトを使用する Bootstrapper.runInArea というユーティリティー・メソッドを作成しました。このメソッドは内部で、指定されたメモリー領域内で短期間存続する RealtimeThread を開始して Runnable を実行します。このブートラップ手法なら、コードが比較的整然とします。
どんなに一生懸命にがんばってみても、このようなブートストラップ・コードを簡潔で整然と読みやすくするのは困難です。コードを読む人にメモリー領域とスレッド・タイプの間でひっきりなしに行われる移動を説明するには、アーキテクチャー図を参照してもらわなければならず、この移動によってベテランのプログラマーでも恐れをなし、途方に暮れるような構成になってしまいます。ここでできる最善のアドバイスは、そのようなコードはローカライズして、開発者向け資料でその背後にある考えを説明するよう努力することです。
これで設計の主要なコンポーネントは完成したので、できあがったアプリケーションを早速、試して見ましょう。
デモ
これまで取り組んできた設計の実装は、この記事に付属しています (ソース・コードをダウンロードしてください)。ソースに目を通して、今まで説明した構想がどのように実行可能コードとして実装されているかを確認することをお勧めします。
監視システムの実装の他、監視システムをテストできるようにダミーの生産ラインと作業員コンソールも用意しました。キャンディーを詰め終えた瓶の重量はガウス分布になるようにし、瓶の充てん量が時に多すぎたり、少なすぎたりするようにしています。
このデモは、メッセージでシステムの状況を示すコンソール・アプリケーションとして実行されます。
デモのビルド
デモ・パッケージには、以下のディレクトリーとファイルが含まれています。
- src -- デモの Java ソース
- build.sh -- デモをビルドするための bash シェル・スクリプト
- MANIFEST.MF -- デモの JAR ファイル用マニフェスト・ファイル
デモをビルドするには、まず任意のディレクトリーにパッケージを解凍し、SweetFactory ディレクトリーに移動して build.sh を実行します。build.sh スクリプトは、WebSphere Real Time で提供された jar、javac、および jxeinajar のバージョンが PATH で有効になっていないと機能しません。
build.sh スクリプトは以下の操作を実行します。
- クラスを保管する bin ディレクトリーを作成
-
javac を使用して Java ソースをビルド
- sweetfactory.jar という実行可能 JAR ファイルをビルド
-
jxeinajar を使用して sweetfactory.jar の AOT コンパイルを実行
ビルド・スクリプトを実行すると、以下の出力が作成されます。
リスト 1. ビルド・スクリプトの出力
[andhall@rtj-opt2 ~]$ cd SweetFactory/
[andhall@rtj-opt2 SweetFactory]$ java -Xrealtime -version
java version "1.5.0"
Java(TM) 2 Runtime Environment, Standard Edition (build pxi32rt23-20070122 (SR1)
)
IBM J9 VM (build 2.3, J2RE 1.5.0 IBM J9 2.3 Linux x86-32 j9vmxi32rt23-20070105 (
JIT enabled)
J9VM - 20070103_10821_lHdRRr
JIT - 20061222_1810_r8.rt
GC - 200612_11-Metronome
RT - GA_2_3_RTJ--2006-12-08-AA-IMPORT)
JCL - 20070119
[andhall@rtj-opt2 SweetFactory]$ ls -l
total 16
-rwxr-xr-x 1 andhall andhall 773 Apr 1 15:41 build.sh
-rw-r--r-- 1 andhall andhall 76 Mar 31 14:20 MANIFEST.MF
drwx------ 4 andhall andhall 4096 Mar 31 14:16 src
[andhall@rtj-opt2 SweetFactory]$ ./build.sh
Working dir = .
Building source
Building jar
AOTing the jar
J9 Java(TM) jxeinajar 2.0
Licensed Materials - Property of IBM
(c) Copyright IBM Corp. 1991, 2006 All Rights Reserved
IBM is a registered trademark of IBM Corp.
Java and all Java-based marks and logos are trademarks or registered
trademarks of Sun Microsystems, Inc.
Found /home/andhall/SweetFactory/sweetfactory.jar
Converting files
Converting /home/andhall/SweetFactory/sweetfactory.jar into /home/andhall/
SweetFactory/aot//sweetfactory.jar
JVMJ2JX002I Precompiled 156 of 168 method(s) for target ia32-linux.
Succeeded to JXE jar file sweetfactory.jar
Processing complete
Return code of 0 from jxeinajar
[andhall@rtj-opt2 SweetFactory]$ ls -l
total 252
drwxrwxr-x 3 andhall andhall 4096 Apr 1 15:42 bin
-rwxr-xr-x 1 andhall andhall 773 Apr 1 15:41 build.sh
-rw-r--r-- 1 andhall andhall 76 Mar 31 14:20 MANIFEST.MF
drwx------ 4 andhall andhall 4096 Mar 31 14:16 src
-rw-rw-r-- 1 andhall andhall 233819 Apr 1 15:42 sweetfactory.jar
|
build.sh スクリプトを実行して作成される sweetfactory.jar は、Sweet Factory デモの AOT コンパイル・バージョンです。
デモの実行
Sweet Factory デモがビルドできたら、いよいよデモを実行できます。このデモは WebSphere Real Time v1.0 の SR1 リリースで実装およびテストされているので、SR1 以降で実行することをお勧めします。
リスト 2. Sweet Factory デモ
[andhall@rtj-opt2 ~]$ java -Xnojit -Xrealtime -jar sweetfactory.jar
Sweetfactory RTJ Demo
Usage:
java -Xrealtime -jar sweetfactory.jar [runtime seconds
[number of production lines [production line period millis] ] ]
Default runtime is 60 seconds
Default number of production lines is 3
Default production line period (time between jars arriving) is 20 milliseconds
No arguments supplied - using defaults
Starting demo
1173021249509: Jar 32 overfilled
1173021250228: Jar 139 underfilled
1173021252770: Jar 521 underfilled
1173021260233: Jar 1640 underfilled
1173021260938: Jar 1746 overfilled
1173021263717: Jar 2162 underfilled
1173021264219: Jar 2238 overfilled
1173021272824: Jar 3528 overfilled
1173021272842: Jar 3529 underfilled
1173021276342: Jar 4054 overfilled
1173021280427: Jar 4667 underfilled
1173021281410: Jar 4815 overfilled
1173021286265: Jar 5542 overfilled
1173021288052: Jar 5810 underfilled
1173021288913: Jar 5940 overfilled
1173021294247: Jar 6739 underfilled
1173021298832: Jar 7426 underfilled
1173021305079: Jar 8362 overfilled
Stopping demo
Run summary:
Production line stats:
Line # Sweet Type Jar Type # of Missed Jars Max Triage Pool Size Min Triage Pool Size
0 Giant Gobstoppers Large 0 10 7
1 Chocolate Caramels Large 0 10 8
2 Giant Gobstoppers Large 0 10 8
Total missed jars: 0
Measurement object pool stats:
Minimum queue depth (degree of exhaustion): 391
Audit stats:
Maximum incoming queue depth: 5
Processing stats:
Total overfilled jars: 9
Total underfilled jars: 9
Total jars processed: 8998
Demo stopped
[andhall@rtj-opt2 ~]$
|
上記の出力を見ると、デモがデフォルトで 3 つの生産ラインを起動し、20 ミリ秒間隔で瓶を送っていることがわかります。
Java VM には -Xnojit オプションを渡して、AOT バージョンのアプリケーションを使用できるようにしていることに注目してください。
デモを実行すると、さまざまな瓶が過剰あるいは過少に充てんされ、タイムスタンプが先頭に付いたメッセージがコンソールに出力されます。最後には表が出力され、それぞれの生産ラインで測定されなかった瓶の数が示されます。
最後の統計は、システムにかかっている負荷の尺度になります。キューの深さの最小値 (Minimum queue depth) は、測定オブジェクト・プールがどれだけ使い果たされたかを示します。プールが空になるとポーリング・スレッドはどこにも受信測定値を保管できなくなるので、瓶の測定ミスが発生することになります。
監査の受信キューの深さの最大値 (Maximum incoming queue depth) が示すのは、監査スレッドによる処理をキューで待機していた測定オブジェクトの最大数です。この値が大きい場合は、監査ロガーに十分な作業時間が与えられなかったためにキューが増加してしまったことを意味します。
Sweet Factory デモでの実験
デフォルトでは、デモはその開発に使用された Opteron ハードウェアの処理能力の範囲内で有効に動作するので、瓶を測定し損なう危険はそれほどありません。その一方、このデモをパラメーター化すれば、生産ラインの数を増やして瓶の到着間隔を短縮できます。
パラメーターを変更することでマシンの処理量を増やすことができます。作業負荷を十分に増やすと、このデモで瓶の測定ミスが発生するようになります。
デモで使用する引数は、実行時間 (秒単位)、生産ラインの数、そして瓶の到着間隔 (ミリ秒単位) の 3 つです。
作業負荷を大きく増やす前に、認識しておく必要があることは、生産ラインの数を増やすと、それに比例してデモ内で実行するスレッドの数も増えるということです。それぞれの NHRT にはスコープが付加されているため、スレッドの数が増えるとスコープ・メモリーの合計スペースも増加し、最終的には枯渇してしまいます。
java-Xrealtime -verbose:sizes -version を実行すると、デフォルトのスコープ・メモリー・スペースの合計が 8MB であることがわかります。
リスト 3. java -Xrealtime -verbose:sizes -version
[andhall@rtj-opt2 SweetFactory]$ java -Xrealtime -verbose:sizes -version
-Xmca32K RAM class segment increment
-Xmco128K ROM class segment increment
-Xms64M initial memory size
-Xgc:immortalMemorySize=16M immortal memory space size
-Xgc:scopedMemoryMaximumSize=8M scoped memory space maximum size
-Xmx64M memory maximum
-Xmso256K OS thread stack size
-Xiss2K java thread stack initial size
-Xssi16K java thread stack increment
-Xss256K java thread stack maximum size
java version "1.5.0"
Java(TM) 2 Runtime Environment, Standard Edition (build pxi32rt23-20070122 (SR1)
)
IBM J9 VM (build 2.3, J2RE 1.5.0 IBM J9 2.3 Linux x86-32 j9vmxi32rt23-20070105 (
JIT enabled)
J9VM - 20070103_10821_lHdRRr
JIT - 20061222_1810_r8.rt
GC - 200612_11-Metronome
RT - GA_2_3_RTJ--2006-12-08-AA-IMPORT)
JCL - 20070119
[andhall@rtj-opt2 SweetFactory]$
|
それぞれのタスクにはスコープ・メモリーの量を気前よく割り当てました。つまり、NHRT ごとに 100KB の割り当てです。各生産ラインには 11 の NHRT を作成します。これを基本に、-Xgc:scopedMemoryMaximumSizeで有効にするのに必要なスコープ・メモリーの合計量を見積り、作業負荷を大きく増やしてみることができます。
例えば、50 の生産ラインを 10 ミリ秒間隔で稼動させるには、少なくとも 55MB のスコープ・メモリーが必要になりますが、多少余裕を持って 60MB にします。このシナリオを 60 秒間実行するためのコマンドは、以下のとおりです。
java -Xrealtime -Xnojit -Xgc:scopedMemoryMaximumSize=60M -jar sweetfactory.jar 60 50 10
|
生産ライン数を十分に増やすと ( 10 ミリ秒間隔でライン数 70 というのが、このシステムでは最大限のようです)、デモで瓶の測定ミスが起こり始めます。瓶の測定ミスが発生すると、以下のようなメッセージがコンソールに出力されます。
Error: measurement pool exhausted
1175439878160 : Missed 20 jars!
|
最初のメッセージは、ポーリング・スレッドがプールから測定オブジェクトを取得しようとして失敗したことが原因となっています。2 番目のメッセージが示すのは、ポーリング・スレッドが最終的に測定オブジェクトを取得するまでに測定されなかった瓶の数です。
このような場合、大部分の CPU 時間は受信測定値の処理に費やされていることになります。つまり、負荷が増加すると、Metronome を実行して監査ログを書き出すだけの時間がなくなるということです。測定値は監査システムの前でキューに蓄積され、測定プールを使い尽くしていきます。測定オブジェクトがなくなり、ポーリング・スレッドが測定オブジェクトを待機せざるを得なくなってはじめて、ロギング・スレッドはログを書き込むための CPU 時間を取得し、測定オブジェクトをプールに返すことになります。
実装のヒントと秘訣
一年間 WebSphere Real Time に取り組んだ末、私たちは RT アプリケーションを最大限に利用するためのヒントと秘訣をまとめました。このセクションでは、私たちが発見したなかで、とりわけ役に立つものを紹介します。
スレッドのタイプとメモリー領域を確認すること
ヒープ以外のメモリーを開発する際には、コードのそれぞれの行で、どのメモリー領域を使用しているのか、そしてどのタイプのスレッドを実行しているのかを十分に意識することが重要です。
java.lang.Threadからメモリー領域に入ろうとするなど、不正な割り当てを実行することによる、わかりにくいバグを紛れ込ませてしまう可能性は大です。
Java SE ではコードに assert()文を書き込んで引数の正常性チェックを行うのが適切なプログラミング慣例であるのと同じように、RT Java コードでは、使用しているスレッド・コンテキストとメモリー領域を表明することが賢明です。
Sweet Factory サンプル・アプリケーションには、checkContext メソッドと、異なるコンテキストを表す一連の定数を提供する専用 ContextChecker クラスが組み込まれています。
エラー処理のための実行可能コードとメモリー領域を用意すること
標準 Java コードでは (その管理されたメモリー環境のおかげで)、エラー処理は単なるコード・ブロックの 1 つでしかありません。一方、ヒープ以外の RT Java となると、エラー処理が大きな頭痛の種となります。
前にも説明したように、NHRT で実行するタスクのほとんどはメモリーを使うため、スコープやプールされたオブジェクトの使用はこれらの特定タスクに合わせて調整する必要があります。
エラーが発生すると、例えばエラー・メッセージを出力するという単純な動作でさえも問題になってきます。この動作を実行するためのメモリーがない可能性があるからです。これに対する選択肢の 1 つは、あらゆる状況でオーバーヘッドを設け、動作が停止する前に数行のデバッグ用の出力をすることですが、実用的な方法とは言えません。
私たちが見つけた最善の方法は、Runnable を拡張するエラー条件ごとのクラスを作成し、障害に関するデータを提供するメソッドを用意することです (これにより、何が起こったのかを理解するのに十分な情報が得られます) 。このクラスのインスタンスは前もって作成しておき、このインスタンスが必要なときにメモリーを消費することなく使えるようにしておきます。また、エラー処理操作を実行するのに十分なスコープ・メモリー領域を用意します。
事前に割り当てられた Runnable オブジェクトと個別のスコープがあれば、エラーが発生した場合でも常にメモリーを使わずに問題をレポートすることができます。このやり方は、オブジェクトをそれ以上作成できなくなって OutOfMemoryError がスローされるような状況で役立ちます。
この手法は、Sweet Factory デモの ProductionLine Poller クラスで実演しています。このクラスでは、プールから Measurementをフェッチできない場合にはerrorReportingRunnableが使用されるように定義しています。
まとめ
この記事では、ますます厳しくなる確定性の要件を満たす RT Java アプリケーションを WebSphere Real Time プラットフォーム で開発し、デプロイする方法を説明してきました。ヒープ以外のメモリーを使用した NHRT プログラミングは、通常のヒープ・ベースのアプリケーションを作成する場合に比べて作業が大幅に増えます。それを示すのが Sweet Factory デモです。同じような関数をヒープ環境で作成するとしたら、ありきたりな作業になっていたことでしょう。Java SE 標準ライブラリーには、スレッド・プールやコレクション・クラスを含め、必要な機能のほとんどが用意されているからです。
NHRT に取り組む際の最大の難関は、習得しなければならない新しい手法がたくさんあるだけでなく、デザイン・パターンの大部分をはじめ、Java SE からやっと学んだベスト・プラクティスの多くが適用不可能で、しかもメモリー・リークの原因となることです。
しかし嬉しいことに WebSphere Real Time を使用すれば、ソフト RT の目標の多くは NHRT でコンストラクターを一切呼び出すことなく達成することができます。また Metronome ガーベッジ・コレクターのパフォーマンスにより、数ミリ秒の精度で実行時間を予測することが可能になります。もし最大限の応答性が必要で、喜んで困難に立ち向かうというのであれば、WebSphere Real Time のヒープ以外の機能がそれを実現してくれるはずです。
ダウンロード | 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|
| Sweet Factory demo for this article | j-rtj5sweetfactory.tgz | 16KB | HTTP |
|---|
参考文献 学ぶために
製品や技術を入手するために
-
WebSphere Real Time: WebSphere Real Time では、正確な応答時間に依存するアプリケーションが、確定性を犠牲にすることなく標準 Java 技術を利用できます。
-
Real-time Java technology: IBM alphaWorks の Real-time Java technology リサーチ・サイトにアクセスして、TuningFork やその他の RT Java を対象とした最先端技術を調べてください。
議論するために
著者について  | 
|  | Caroline Gough は、小さなソフトウェア会社で開発者として 3 年の経験を積んだ後、IBM Hursley Laboratory の Java Technology Centre System Test チームに加わりました。チームでは、負荷テストおよび RAS (信頼性、可用性、および保守容易性) ツールの専門知識を持つシニア・テスターとして活躍しています。IBM WebSphere Real Time V1.0 に取り組んだ彼女は現在、今後の Java プラットフォーム・リリースを対象としたテストを作成しています。 |
 | 
|  | Andrew Hall は、University of Southampton で人工知能による電子工学を学んだ後、2004年に IBM Java Technology Centre に入社しました。それからの 2 年間は、Java System Test チームで WebSphere Real Time V1.0 を含めた Java ランタイムのテスト自動化および負荷テストを中心に取り組み、現在は Java 5.0 Service Team のメンバーとなっています。趣味は読書、写真、そしてジャグリングです。 |
 | 
|  | Alan Stevens が IBM Hursley Laboratory に入社したのは 1988年です。それ以来、CICS や WebSphere などの IBM 製品および IBM Java 技術のパフォーマンス、スケーラビリティー、そして確定性の改善を専門としています。Java ツールの広範な分野での経験を持ち、JSR 163 (JVMTI 定義) では IBM 代表を務めています。現在、IBM WebSphere Real Time Java パフォーマンス・チームのリーダーです。 |
 | 
|  | Alan Stevens が IBM Hursley Laboratory に入社したのは 1988年です。それ以来、CICS や WebSphere などの IBM 製品および IBM Java 技術のパフォーマンス、スケーラビリティー、そして確定性の改善を専門としています。Java ツールの広範な分野での経験を持ち、JSR 163 (JVMTI 定義) では IBM 代表を務めています。現在、IBM WebSphere Real Time Java パフォーマンス・チームのリーダーです。 |
記事の評価
|