Hello World! コンテンツ・メディア第1事業部の、jyukutyoこと阪田です。 2日目からいよいよ本格的なセッションが始まります。すべてのセッションについては書けないので、いくつかを抜粋して書いていきます。私の興味はJVMにあり、Javaの新しい言語仕様よりもGCやパフォーマンスチューニングのセッションに多く出ました。

GC Tuning Confessions of a Performance Engineer

パフォーマンスエンジニアによるGCのチューニングについてのお話です。やはり日本で英語スピーカーが話すときとまったく話すスピードが違います。すべてを理解しようとせず、ヒアリングと資料の内容を照らし合わせながら必死に理解を進めました。

GCにおけるメトリクスには、たとえば次のようなものがあります。

  • GCのイベント数
  • 最小/最大実行時間
  • 平均実行時間
  • 99%平均実行時間

GCの内部に迫っていくと、GCがあるからといってメモリリークがなくなるわけでないということです。そしてGCにはトレードオフがあります。今のアプリケーションはどんどんヒープ領域が大きくなっていますが、それゆえにフルGCは怖いものとなっています。GCのチューニングの観点からは、登場人物が2人います。スループットとレイテンシです。GCには世代別や並列などの種類がありますが、OpenJDKではすべてのGCは世代別GCです。 アプリケーションがレイテンシを重視しているか、GCを並列にするかどうか、さらにGCでコンパクションするかどうかでCMS(Concurrent Mark-Sweep)かG1GCにするかを選択します。

GCアルゴリズム

ここからはGCアルゴリズムの復習です。まずヒープ領域はEden、Survivor0、Survivor1を合わせたYoung領域と、残りのOld領域に分けられます。Young領域とOld領域では、GCのアルゴリズムを別々にできます。 シリアルコレクタであれば、YoungもOldも両方シングルスレッドでGCします。スループットコレクタ(パラレルコレクタ)はYoungもOldもマルチスレッドでGCし、コンパクションもします。CMSはYoungはことごとくGCし、Oldはmostly concurrent(”ほぼ”並列)にGCします。 プロモーション(昇格)とはYoungからOldへの昇格を指します。CMSはOldへのプロモーションが早くなります。またCMSではフリーリストを使って領域を管理しています。もし割り当てられる領域がなかったら、GCはfailureとなります。コンカレントのサイクルが終わる前にOldがいっぱいになることがあり、そのときはスペースを空けます。CMSはフラグメンテーションが発生します。

G1GC

G1はリージョナルヒープです。リージョンごとに世代があります。GC時に特定のリージョンにオブジェクトを集めます。クリーンアップフェーズでリージョンが空になれば、別の世代のリージョン(同じでもよいが)として再利用します。G1にはYoungもOldもすべてGCするmixed collectionがあります。もしすべてのリージョンがいっぱいになってしまえば、evacuation failureとなります。evacuation failureになってしまうととても高コストな処理となり、すぐにフルGCが走ります。 GCのチューニングについてのアドバイスは「あまりチューンしない」ということです。サイズと希望停止時間(pause time goal)ぐらいを指定するだけでよいです。あとは、初期ヒープ占有の閾値が問題を起こすかもしれません。マーキングサイクルが長い場合に並列マークスレッドを増やすくらいでよいでしょう。YoungのTo領域がevacuation failureを引き起こしている場合は、-XX:G1ReservePercentの値を増やす方がいいです。 G1にもフラグメンテーションがあります。デフォルトでは5%ほどです。さらに巨大オブジェクトの問題もあります。巨大なオブジェクトをヒープに割り当てるとき、そのサイズがリージョンの50%未満なら単純に割り当てるだけです。50%以上でも割り当てられますが、もしサイズがリージョンより大きい場合はどうなるでしょうか?隣接した複数のリージョンを使います。そうすると、フラグメンテーションは発生してしまいます。ただし、巨大オブジェクトは数は少なく生存期間が短いことが多いので、問題にならないでしょう。もし数が多く生存期間が長ければ、evacuation failureを引き起こすかもしれません。Oldの占有率を増やしているでしょうから。 G1のGCログを見たとき、remarkポーズ時間が長く、Ref Procがあるならオプションをつけた方がよいでしょう(何のオプションか不明)。

セッションのまとめ

デフォルトのGCであれば、TenuringThresholdで世代サイズを設定しましょう。-XX:+UseAdaptiveSizePolicyも必要です。スループットにはYoung領域のサイズが重要です。 CMSであれば、Fractionと-XX:+UseCMSInitiatingOccupancyOnlyがマーキングの閾値を設定する助けになります。閾値はOld領域の占有率のパーセンテージで表現されます。CMSではプロモーションが早く、それがフラグメンテーションを発生させる原因ともなっています。 G1は、最大希望停止時間(max pause time goal)が重要です。あとはデフォルト値をしっかり知ることが大切です。G1はヒープ領域を2048のリージョンにわけ、各リージョンのサイズは1MBから32MBの間になります。アグレッシブな停止時間は設定しないこと!オーバーヘッドが増えるだけです。 最後はJavaパフォーマンス(http://www.amazon.co.jp/dp/4873117186/)読もうね!で終わりました。

jyukutyoコメント

JDK9ではG1がデフォルトになる予定のようです。ただ、CMSとG1のどちらを使うかという判断基準が私にはまだわかっていません。OpenJDKでのトラブルシューティングをされている方に話をうかがったところ、ヒープが8GBを超えるようならG1、それ以下であればリージョンが小さくなるためCMSでよい、と教えていただきました。ただし、CMSでフラグメンテーションが問題となるようなときは8GB未満でもG1でよいかもしれません。
ヒープが8GB未満であればG1を選択するメリットがないので、CMSでよい。8GB以上でレスポンスタイムを一定以下に抑えたいならG1、全体的なスループットを上げたいならCMSだがフラグメンテーションが起きるならG1にする。8GB以下でフラグメンテーションが起きるなら Parallelを選ぶのが基本戦略と改めて教えていただきました!ありがとうございます!