java hotspot vmコード・キャッシュについて - …//java architect /...

4
ORACLE.COM/JAVAMAGAZINE ///////////////////////////// JULY/AUGUST 2013 JAVA TECH 37 COMMUNITY JAVA IN ACTION ABOUT US blog //java architect / J ava HotSpot VM には極めて高 度な Just-In-Time(JIT)コン パイラが搭載されています。この コンパイラにより、Java HotSpot VM が稼働するすべてのプラット フォームに対して、高度に最適化 されたマシン・コードを生成でき ます。 本記事では、Java HotSpot VM の JIT コンパイラの 1 つの重要な 側面である、コード・キャッシュ の秘密を解き明かします。コード・ キャッシュを理解することで、他 の方法では追跡が困難なさまざ まなパフォーマンス問題を把握で きるようになります。 注:Java Magazine の過去の 記事「Java HotSpot VM におけ る JIT コンパイルの概要」と「Java HotSpot VMの内部を探る(2): パフォーマンス解析のための統 計情報」では、Java HotSpot VM および JIT コンパイラの入門レベ ルのトピックについて取り上げて います。 JIT コンパイラおよびコード・ キャッシュの話を始める前に、ま ずは Java メソッドのライフサイク ルについて考えます。 Java メソッドのライフサイクル Java プラットフォームで実行中の プログラムに新たにロードおよび リンクされるコードの最小単位は クラスです。そのため、新しいメ ソッドが読み込まれるときには、 そのメソッドを含むクラスのクラ ス・ローディング・プロセスが必 ず実行されます。 このクラス・ローディング・プ ロセスはピンチ・ポイント、すな わち Java プラットフォームのセ キュリティ・チェックが多数集中 する場所として機能します。した がって、Java メソッドのライフサ イクルは、実行中の Java 仮想マ シン(JVM)に新しいクラスを読 み込むクラス・ローディング・プ ロセスから始まります。 クラス・ローディング クラス・ローディングは、(通常 はディスクから読み込まれる)バ イト・データのストリームから始 まります。このストリームはクラス・ ファイル形式である必要がありま す。バイト・ストリームがクラス・ ファイル形式の場合は、クラス・ ローダーでリンク処理を実行でき ます。 このリンク処理のプロセスは複 数のフェーズで構成されます。第 1 のフェーズは検証 フェーズで、これは もっとも重要なフェー ズでもあります。検 証フェーズとは、新 しいクラス・ファイル が Java の堅牢なプロ グラミング・モデル を侵害しないことを JVM で確認するため のフェーズです。 検証フェーズの間 に、多数のセキュリティ制約が チェックされます。検証内容の例 を以下に挙げます。 メソッドがアクセス制御キー ワードを正しく使用しているこ 正しい静的型を指定してメソッ ドが呼び出されていること 適切に型付けされた値のみが 変数に代入されていること 変数が使用される前に適切に 初期化されていること メソッドのバイトコードに対し ても広範なチェックが実行されま す。ここでの重要なポイントは、 JVM がスタック・マシ ンであることです。 この方式は熟考を 重ねて選択されまし た。レジスタベース のマシンと比較して、 スタック・マシン上で はセキュリティなどに 関するプロパティの正 当性を非常に容易に 証明できます。 したがって、バイト コードに対するチェックの大部分 を、クラス・ローディングの時点 で静的な解析により経済的に実 行できます。その結果、危害をも たらすコードが実行中の JVM に 入り込む可能性が非常に低くなり ます。 Java HotSpot VM コード・キャッシュについて コード・キャッシュのあふれを検出し、回避する方法を学ぶ 写真:JOHN BLYTHE BEN EVANS キャッシュがあふれた コード・キャッシュが あふれた場合は、言で言えば、コンパ イルが停止します。 Ben Evans (@kittylyst): jClarity の CEO であり London Java Community 主催者。 Java SE/EE Executive Committee の メンバーでも ある。Martijn Verburg 氏 とともに、 『The Well- Grounded Java Developer』 を執筆。

Upload: others

Post on 14-Apr-2020

2 views

Category:

Documents


0 download

TRANSCRIPT

ORACLE.COM/JAVAMAGAZINE ///////////////////////////// JULY/AUGUST 2013

JAVA

TEC

H

37

COMMUNITY

JAVA IN ACTION

ABOUT US

blog

//java architect /

Java HotSpot VM には極めて高度な Just-In-Time(JIT)コンパイラが搭載されています。このコンパイラにより、Java HotSpot VM が稼働するすべてのプラットフォームに対して、高度に最適化されたマシン・コードを生成できます。本記事では、Java HotSpot VMの JITコンパイラの 1つの重要な側面である、コード・キャッシュの秘密を解き明かします。コード・キャッシュを理解することで、他の方法では追跡が困難なさまざまなパフォーマンス問題を把握できるようになります。

注:Java Magazine の過去の記事「Java HotSpot VM における JITコンパイルの概要」と「Java HotSpot VM の内部を探る(2):パフォーマンス解析のための統計情報」では、Java HotSpot VMおよび JITコンパイラの入門レベルのトピックについて取り上げています。JITコンパイラおよびコード・

キャッシュの話を始める前に、ま

ずは Javaメソッドのライフサイクルについて考えます。

JavaメソッドのライフサイクルJavaプラットフォームで実行中のプログラムに新たにロードおよびリンクされるコードの最小単位はクラスです。そのため、新しいメソッドが読み込まれるときには、そのメソッドを含むクラスのクラス・ローディング・プロセスが必ず実行されます。 このクラス・ローディング・プロセスはピンチ・ポイント、すなわち Javaプラットフォームのセキュリティ・チェックが多数集中する場所として機能します。したがって、Javaメソッドのライフサイクルは、実行中の Java 仮想マシン(JVM)に新しいクラスを読み込むクラス・ローディング・プロセスから始まります。

クラス・ローディングクラス・ローディングは、(通常はディスクから読み込まれる)バイト・データのストリームから始

まります。このストリームはクラス・ファイル形式である必要があります。バイト・ストリームがクラス・ファイル形式の場合は、クラス・ローダーでリンク処理を実行できます。このリンク処理のプロセスは複数のフェーズで構成されます。第1のフェーズは検証フェーズで、これはもっとも重要なフェーズでもあります。検証フェーズとは、新しいクラス・ファイルが Java の堅牢なプログラミング・モデルを侵害しないことをJVMで確認するためのフェーズです。 検証フェーズの間に、多数のセキュリティ制約がチェックされます。検証内容の例を以下に挙げます。

■ メソッドがアクセス制御キーワードを正しく使用していること

■ 正しい静的型を指定してメソッ

ドが呼び出されていること ■ 適切に型付けされた値のみが変数に代入されていること

■ 変数が使用される前に適切に初期化されていることメソッドのバイトコードに対しても広範なチェックが実行されます。ここでの重要なポイントは、

JVMがスタック・マシンであることです。 この方式は熟考を重ねて選択されました。レジスタベースのマシンと比較して、スタック・マシン上ではセキュリティなどに関するプロパティの正当性を非常に容易に証明できます。 したがって、バイト

コードに対するチェックの大部分を、クラス・ローディングの時点で静的な解析により経済的に実行できます。その結果、危害をもたらすコードが実行中の JVMに入り込む可能性が非常に低くなります。

Java HotSpot VMコード・キャッシュについて

コード・キャッシュのあふれを検出し、回避する方法を学ぶ

写真:JOHN BLYTHE

BEN EVANS

BIO

キャッシュがあふれた コード・キャッシュが

あふれた場合は、一

言で言えば、コンパ

イルが停止します。

Ben Evans(@kittylyst):jClarity のCEOでありLondon Java Community主催者。Java SE/EE Executive Committeeのメンバーでもある。Martijn Verburg 氏とともに、『The Well-Grounded Java Developer』を執筆。

ORACLE.COM/JAVAMAGAZINE ///////////////////////////// JULY/AUGUST 2013

JAVA

TEC

H

38

COMMUNITY

JAVA IN ACTION

ABOUT US

blog

//java architect /

たとえば、レジスタの内容を追跡しなくても、メソッドのあらゆる地点でスタックの状態を推測できます。 注意点として、パフォーマンス上の理由から、JDK 関連のクラス(rt.jar に含まれるもの)はチェックされません。JDK 関連のクラスは、原始クラス・ローダーによってロードされます。原始クラス・ローダーは包括的なセキュリティ・チェックを実行しません。このようにクラス・ローディングをバイトコード検証の機会として利用することで、クラス・ローディング・プロセスの速度は低下します。しかし、その見返りとして、実行時の速度は飛躍的に向上します。一度チェックを行っておけば、コードを実行するときにチェックを省略できるからです。

Java HotSpot VMでのクラス・ローディングの実装方法バイト・ストリームをクラス・オブジェクトに変換するために使用する重要なメソッドは、JavaメソッドのClassLoader::defineClass() です。このメソッドはネイティブ・メソッドのClassLoader::defineClass1() に処理を委譲し、このネイティブ・メソッドでは基本的なチェックと文字列変換を行った後に JVM_DefineClassWithSource()というC関数を呼び出します。お分かりのとおり、この C関数がJVMへのエントリ・ポイントとなり、この C関数を通じて Java HotSpot VM のC++コードにアクセスできます。Java HotSpot VMでは SystemDictionaryを使用して、ClassFileParser のparseClassFile() メソッドにより新しいク

ラスをロードします。クラス・ローディングが完了すると、メソッドのバイトコードが C++オブジェクト(methodOop)内に配置され、バイトコード・インタプリタで使用できる状態になります。 この処理はメソッド・キャッシュと呼ばれることもありますが、実際にはパフォーマンス上の理由から、バイトコードはmethodOop 内にインラインで保持されます。

メソッドのコンパイル方法Java HotSpot VM のバイトコード・インタプリタ内では、多数のパフォーマンス・カウンタやトレース・カウンタが管理されます。サーバー・コンパイラの場合、メソッドは10,000回実行されるとコンパイルされます。コンパイラから出力されるコードはマシン・コード(特定のオペレーティング・システムおよび CPUで使用するための専用コード)です。マシン・コードは、中心的な要素であるCodeCache(C++オブジェクト)内に配置されます。CodeCache はCodeBlobインスタンス(コンパイル後のメソッド・コードの表現)を保持するヒープのような構造体です。コード・ブロブがコード・キャッシュ内に配置されると、実行中のシステムが、インタプリタ・モードから、新しくコンパイルされたコードを使用するモードに切り替えられます(この切り替えの処理はポインタ書き換え(pointer

swizzling)と呼ばれることもあります)。

PrintCompilationJITコンパイル・サブシステムの制御に使用できる非常にシンプルなフラグの 1つが -XX:+PrintCompilationです。このスイッチを指定することで、JITスレッドによってコンパイル関連のメッセージが標準ログに追記されるようになります。

PrintCompilation については、「Java HotSpot VM における JITコンパイルの概要」の記事で紹介しました。

脱最適化Java HotSpot VM のサーバー・モードで実行される最適化は、常に妥当であるとは限りません。そのため、サニティ・チェック(ガード条件と呼ばれることも多い)を使用して、最適化が適切に行われるように保護します。このチェックが失敗した場合は、妥当でない推測に基づいて生成されたコードの脱最適化を実行します。

その後、Java HotSpot VM によって、再検討の後にまた別の最適化が試されることもよくあります。そのため、同じメソッドに対して脱最適化と再コンパイルが何度か実行されることがあります。脱最適化イベントは、PrintCompilation ログで「made not entrant」や「made zombie」などの行により示されます。 これらの行は、すでにコンパイルされてコード・ブロブが生成されていた

特定のメソッドが、脱最適化されたことを示します。脱最適化は一般的に、新しいクラスがロードされ、Java HotSpot VM による推測が無効になったことが原因で実行されます(ただし、別の原因の場合もあります)。

プログラムの準備中の処理Javaプログラムが起動して初期化フェーズが終了した後は、通常は平常運用に移行し、コードのホット・パスが現れ始めます。PrintCompilationスイッチをオンにし

てプログラムを複数回実行し、コンパイルされたメソッドに関するログを収集した場合、以下のようなパターンが浮かび上がります。

■ コンパイルは通常、最終的に停止する

■ コンパイルされたメソッドの数が安定する

■ 同じプラットフォーム上で同じテスト入力に対してコンパイルされた一連のメソッドは、通常はかなり一致する

■ コンパイルされたメソッドの細部は、実際に使用する JVM、オペレーティング・システム・プラットフォーム、CPUによって異なる 注:ある特定のメソッドのコンパイル済みのコードが、すべてのプラットフォームで同程度のサイズになることは保証されていません。 上記のパターンは一般的なものですが、通常とは異なる様相を示す場合もあるため、常にチェックを行う必要があります。便利なチェック方法の 1つが Java VisualVMを使用することです。Java VisualVMの Classes セクション(図

ご存じでしたか ?最近の JDK のバー

ジョン(Java 7

Update 4 以降)で

は、投機的フラッ

シュという新たな

コード・キャッシュ・

フラッシュの方法が

追加されました。

ORACLE.COM/JAVAMAGAZINE ///////////////////////////// JULY/AUGUST 2013

JAVA

TEC

H

39

COMMUNITY

JAVA IN ACTION

ABOUT US

blog

//java architect /

1)には、クラス・ローディングの概要を示すグラフが示されます。

コード・キャッシュがあふれた場合の動作一言で言えば、コンパイルが停止します。停止の原因は、コンパイル後のコード・ブロブをコード・キャッシュから削除する方法は通常、脱最適化しかないためです。 コード・キャッシュ領域は、コード・キャッシュから無効となった「zombie」コード・ブロブをフラッシュすることで解放されます(呼出し元の変更などにより今後は使用されなくなった「not entrant」ブロブは、コードがフラッシュできるようになったときに zombieとなります)。最近の JDK のバージョン(Java 7 Update 4 以降)では、投機的フラッシュという新たなコード・キャッシュ・フラッシュの方法が追加されました。このアプローチでは、古いメソッドがフラッシュの対象としてマークされ、このメソッドを保持するmethodOopとのリンクが切られます。このコンパイル済みのメソッドをVMで呼び出すことが必要になった場合、メソッドが再度methodOop にリンクされ、フラッシュの対象から外されます。 しかし、このメソッドがまたしばらくの間呼び出されない場合は、methodOopはインタプリタ・モードに戻り、コード・ブロブがフラッシュの対象になります。

起動時の動作アプリケーションの起動時間がコード・キャッシュにとって問題になりうる原因

を理解するために、想像上の Springアプリケーションを取り上げます。 Springアプリケーションは、Bootstrapクラスを使用して起動します。このクラスは、作成と組み立てを要するインスタンスの詳細を記載したXMLファイルを探します(したがって、このファイルにはロード対象のクラスが定義されます)。 そのため、Springアプリケーションの

クラス・ローディングには 2つのフェーズがあります。第 1フェーズではブート

ストラップの開始に必要なクラスをロードし、第 2フェーズではアプリケーション・クラスを通常どおりロードします。JITコンパイルの観点からは、このよ

うに 2つのフェーズがある点は重要です。Springフレームワークでは、ロードとインスタンス化の対象となるクラスを検出するために、リフレクションやその他のさまざまな技術を駆使していることがその理由です。このようなフレームワーク関連のメソッドはアプリケーション起動時に頻繁に呼び出されますが、

その後はまったく使用されません。あるSpringフレームワークのメソッドがコンパイル対象となるほどの回数実行されたとしても、アプリケーションからはそのメソッドはほとんど利用されません。その結果、アプリケーションの起動時間はわずかに向上しますが、その代償として、稀少なリソースが使い果たされてしまいます。フレームワークのメソッドが大量にコンパイルされた場合、コード・キャッシュ全体が使い果たされ、本当にコンパイルする必要があるアプリ

図1

ORACLE.COM/JAVAMAGAZINE ///////////////////////////// JULY/AUGUST 2013

JAVA

TEC

H

40

COMMUNITY

JAVA IN ACTION

ABOUT US

blog

//java architect /

ケーションのメソッドのために使用する領域がなくなる可能性があります。このような問題を解決するために、JVMではカウンタ減算の手法を使用します。もっとも単純な方法では、30 秒ごとに 50%ずつ、メソッド呼出しのカウンタ値を減らします。 つまり、メソッドが起動時のみに使用

される場合、メソッド呼出しのカウンタ値は数分以内に、事実上ゼロまで減算されることになります。その結果、使用頻度の低い Springフレームワークのメソッドが貴重なコード・キャッシュ領域を消費する事態を回避できます。より一般的には、この手法で、使用頻度のごく低いメソッド(lukewarm method)がキャッシュされることを防ぎます。

コンパイルとコード・キャッシュを制御するスイッチコンパイルとコード・キャッシュを制御するスイッチは以下のとおりです。 ■ -XX:+PrintCompilation:コンパイル・イベントと脱最適化イベントに関するログ・エントリを表示

■ -XX:CompileThreshold=n:メソッドがコンパイルされるための条件となるメソッド呼出し回数を変更

■ -XX:ReservedCodeCacheSize=YYm:使用するコード・キャッシュの総サイズを設定

■ -XX:+UseCodeCacheFlushing:利用頻度の低いコード・ブロブのフラッシュを JVMに許可(Java 7 Update 4以降ではデフォルトでオン)

コード・キャッシュがあふれる可能性のあるアプリケーションの検出方法

まず、キャッシュが実際に早い段階であふれることを確認します。「compilation halted」メッセージが生成されたことを確認するか、以下の手順を実行します。

1. -XX:+PrintCompilation を使用して、実際にコンパイルされているメソッドを出力します。

2. この出力が安定状態になるまで待機します。

3. 数回の実行を繰り返し、結果セットが安定するかを確認します。

4. -XX:ReservedCodeCacheSizeを使用して、コード・キャッシュのサイズを増やしてみます(まずは 2倍にすると良いでしょう)。コンパイルされたメソッドの数が増加した場合は、元のコード・キャッシュが小さすぎたと見なすことができます。

5. 全体的なパフォーマンスの再テストを実行し、コード・キャッシュのサイズを増やすことでアプリケーション・パフォーマンスの他の側面に悪影響が出ないことを確認します。

まとめ本記事では、JITコンパイルされたコードを格納するための Java HotSpot VM内の領域であるコード・キャッシュについて説明しました。クラス・ローディングによるメソッドのロード方法を確認し、コンパイル、最適化とその結果に応じた脱最適化、フラッシュというメソッドのライフサイクルを確認しました。また、コンパイルの戦略に影響するアプリケーションの起動の問題や、Java HotSpot VMによるその問題の回避方法

についても確認しました。 さらに、コンパイルを制御するためのいくつかのスイッチについて説明し、アプリケーション・コードで JITコンパイルを存分に活用するためにコード・キャッシュのあふれを検出する方法と回避する方法についても確認しました。</article>

LEARN MORE• 「 Java HotSpot VMにおけるJITコンパ

イルの概要」

• 「 Java HotSpot VMの内部を探る(2): パフォーマンス解析のための統計情報」

One of the most elevating things in the world is to build up a community where you can hang out with your geek friends, educate each other, create values, and give experience to your members.Csaba Toth

Nashville, TN Java Users' Group (NJUG)

FIND YOURJUG HERE

LEARN MORE