<< セッション共有について | main | ITシステムにおける性能試験について >>

スポンサーサイト

一定期間更新がないため広告を表示しています

| - | - | - |

Javaヒープサイズのチューニングについて(世代別の場合)

はじめに

Javaオブジェクトが使うメモリ領域のことをJavaヒープと呼ぶ。

Javaアプリケーション実行時は、Javaオブジェクトを大量に生成・破棄することになるが、Java Virtual Machine (JVM)は、Javaヒープが空いている限りはJavaオブジェクトを作成し続ける。Javaヒープがオブジェクトでいっぱいになると、JVMはGarbage Collection (GC)という処理を実行して、使い終わったオブジェクトが占有していたメモリ領域を、新規のJavaオブジェクト作成用に再利用できるようにする。

GC実行中はアプリケーションの処理が概ね停止するので、如何に、パフォーマンスとスループットに悪影響を与えないようにGCを実行させるか、ということを目的としてチューニングする必要がある。

ちょっとしたユーティリティ類を動かすだけなら、せいぜい数百MB程度のJavaヒープで十分であり、GCが発生しても数百ミリ秒程度なので、気になることはまずない。しかし、業務システムの場合は、大量のデータと大量のトランザクションを処理するために、かなりのサイズのJavaヒープが必要となることが多く、GC実行時間が秒単位に延びてしまうことがある。

昔は、メモリが高価であり、アドレッシングも32bitだったので、2GB程度のJavaヒープで何とかやりくりしていたため、負荷が高いシステムでは、Javaヒープが極短い時間でいっぱいになり、GCが1秒間に何度も実行され、1回当たり数百ミリ秒〜数秒掛かるので、スループットが悪化し、最悪の場合はOutOfMemoryErrorに至ってしまうということがあった。

今では、64bitモードの広大なアドレス空間が利用可能で、メモリも安価になったので、深刻な問題に遭遇することは少なくなった。しかし、現在でも、高負荷で複雑なアプリが稼働する環境では、GC実行方式とJavaヒープサイズをチューニングすることの効果は大きく、システムの安定稼働のために重要な営みである。(逆に言うと、大して負荷が高くなく、アプリもそれほど複雑でなく、Javaヒープも潤沢に確保できる場合は、JVMがかなり自動でよろしく計らってくれるので、チューニングしてもそれほど何かが良くなるというものではない。)

GC方式の選択

GC方式は複数の種類から選択可能であるが、現状では世代別GCが最も多く使われていると思われる。Javaヒープサイズのチューニングは、アプリケーションの特性に応じて個別に検討する必要があるが、「NEWを大きく、OLDを小さく」、「サイズの差はOLDでつけて、NEWのサイズは固定」、「物理メモリが余っていても、全体サイズを大きくし過ぎない」といった原則に基づいてチューニングしていくことになる。

なお、Javaヒープサイズが極端に大きい、もしくは小さいなどの顕著な特徴がある場合は、他の方式のGCを検討する必要がある。選択できるGC方式は、Javaランタイムの種類ごとに異同があるので、JVM開発ベンダーのドキュメントを参照する必要がある。

世代別GCのメモリ構造と基本的な動作

詳細はJVM開発ベンダーに依存するが、世代別GCではプロセスが確保する全メモリ空間を、以下のように分類して管理する。

JVMのメモリーレイアウト図

領域説明
JavaヒープNEW領域(nursery)EdenJavaオブジェクトが新規に作成された際に、まずアロケーションされる領域。
SurvivorEdenスペースがいっぱいになった時点で、生存していたオブジェクトが残される領域。EdenとSurvivorの間で生存オブジェクトがコピーされることをフリップと呼ぶ。 アプリケーションの特性に依存するが、フリップによって生き残るオブジェクト数は少数であることが多く、NEW領域においてSurvivor領域に必要なサイズは、Eden領域よりも少なくて済む。この比率は、JVMが自動的に計算して動的に変動する。(多くの場合、NEW領域の大半はEden用に使われる。)
OLD領域(tenured)NEW領域で何度かフリップが繰り返される間、存続し続けたオブジェクトは、長命オブジェクトとみなされてOLD領域にコピーされる。OLD領域がいっぱいになると、フルGC(もしくはGlobal GC)と呼ばれるGCが実行される。NEW領域のGCは、EdenとSurvivor間でのコピーGCだったが、OLD領域のGCは、従来のマーク&スイープ型GCであり、必要に応じてコンパクションも実行される。なお、ラージオブジェクトとそれ以外に分けられ、OLD領域に占めるラージオブジェクトアロケーション用の領域の比率は、JVMが自動的にチューニングするが、このサイズが頻繁に増減するアプリの場合は、ラージオブジェクトエリアのサイズを明示的に指定した方が良い。
Nativeヒープクラス定義、メソッド定義、ダイレクトバイトバッファ、スタック、JVM自身の稼働領域などで使われる。Javaヒープの最小・最大のサイズは、java起動時引数-Xms-Xmxで指定するが、これではNativeヒープのサイズは制限されず、上限は当該OS区画に割り当てられている物理メモリサイズになる。アプリケーションの特性によっては、ダイレクトバイトバッファや、スタックのサイズを指定する必要が生じることがある。この領域のメモリメンテナンスは、ダイレクトバイトバッファ枯渇か、フルGC時に行われる。

NEW領域は、EdenとSurvivorに分けられる。JavaオブジェクトはEdenにアロケーションされるが、いっぱいになると、生存オブジェクトをSurvivorへコピーする。

動作の詳細はJVMベンダーに依存し、Oracle JVMの場合は、Survivor領域を二つに分けて、EdenからSurvivorへの生存オブジェクトのコピーは一時点では一方へ行い、EdenかSurvivorがいっぱいになると、他方のSurvivorへのコピーが実行される。IBM JVMの場合は、Edenがいっぱいになると生存オブジェクトをSurvivorへコピーし、今度はSurvivor領域をEden領域としてアロケーションして行き、以後、いっぱいになる度に入れ替えを繰り返す。何れにしても、NEW領域のGCは、コピーGC(Copying GC、もしくはScavenger GC)と呼ばれる。

NEW領域での、EdenとSurvivor間でのコピーを何度か繰り返しても存続し続けるオブジェクトは、OLD領域にコピーされる。OLD領域がいっぱいになると、フルGCが発生する。一般に、NEW領域のGCよりも、OLD領域のGCの方が遅く、アプリケーションの振る舞いに対して与える影響が大きい。

通常のアプリケーションでは、オブジェクトの大半は、短命オブジェクトだ。これらのオブジェクトは、メソッドブロック内や、長くてもトランザクションスコープを存続期間とするものであり、NEW領域で何度かフリップを繰り返すうちに消滅し、OLD領域へ昇格することはない。OLD領域へ昇格するのは、極わずかな長命オブジェクトだけだ(長命なものとして、セッションスコープ、アプリケーションスコープのオブジェクトが存在する)。しかし、NEW領域が小さいと、数百ミリ秒以下の間隔でEden領域がいっぱいになってしまい、短命であるにもかかわらず、OLD領域に昇格され、OLD領域がすぐいっぱいになり、フルGCが頻繁に発生することになってしまう。

短命オブジェクトをNEW領域のGCで確実に消滅させ、OLD領域のGCが発生しても短時間で完了させるために、NEW領域を十分に大きくし、OLD領域を必要十分以上に大きくしないことが、重要になる。NEW領域の比率を、全体の1/3程度としているケースが多いが、適切なサイズはオブジェクトのサイズ・個数・生存時間に依存する。オブジェクトのサイズが十分に小さく、生存時間が十分に短いことが分かっているアプリケーション以外では、NEW領域の方をOLD領域よりも大きくした方が適切な場合が多い。

本ページでは、主に、JavヒープのNEW領域とOLD領域のチューニング方針について記載するが、Nativeヒープについても、チューニングする必要があることがある。Javaヒープのサイズや使用状況は、冗長GCログから評価するが、Nativeヒープのサイズは、Linux/UNIX系コマンド"svmon -P"出力結果や、Javaプロファイラによって評価する必要がある。

世代別GCのチューニング方針

世代別のGCは、上記の通り、「NEW領域を大きく、OLD領域は小さく」することが重要だ。NEW領域を大きくすることで、OLD領域のGC発生頻度を減少させることができるし、OLD領域を小さくすることで、1回当たりのGC実行時間を短縮できるためだ。但し、OLD領域を小さくし過ぎると、OutOfMemoryErrorが発生してしまうので、必要十分なサイズに設定することが必要だ。

まず、類似すると考えられるアプリが実稼働している環境があるなら、当該環境をリファレンスして、最初の仮定値を決定するのが良い。他システムの設定をリファレンスすることができず、当該OS区画で利用可能な物理メモリの上限が決まっている場合は、以下のような感じで初期設定する。設定オプションは、Javaランタイムの種別ごとに異動があるので、上記のベンダー提供マニュアルを参照する必要がある。

  1. OS区画で利用可能な物理メモリの使用率に十分余裕がある中で、Javaプロセス全体で利用可能なサイズを決定する(ページングが発生しないように、OS+プロセスが実際に利用するメモリサイズが、物理メモリ全体の70〜80%程度になる程度で見積もる)。
  2. Javaプロセスで利用可能なサイズの2/3程度をJavaヒープサイズの最大値(-Xmx)に設定する。(残りの1/3がネイティブヒープで使われる想定。)
  3. Javaヒープサイズの最大値の70%程度を最小値(-Xms)にする。
  4. Javaヒープサイズの最小値の半分程度をNEW領域のサイズとする(NEW領域は最小=最大とする)。
  5. Javaヒープの全体から、NEW領域のサイズを引いたものが、OLD領域のサイズとなる。

Javaヒープサイズの設定を、上記のように、最小値(-Xms) < 最大値(-Xmx)とする場合は、差をつけるにはOLD領域のサイズで差をつけ、NEW領域サイズは固定にした方が良い。NEW領域のGCと比べると、OLD領域のGCの方が、まれにしか実行されないためだ(頻繁にサイズ拡大・縮小してほしくないため)。

次に、実際にアプリケーションをデプロイして、本業務ピーク時相当の負荷を掛けたときの冗長GCログを評価することで、以下の方針で、OLD領域のサイズを縮小し、NEW領域のサイズを拡大する。

  • OLD領域の、GC後のメモリ使用量が、OLD領域の最小値の70%程度になるまで、OLD領域の最小値を下げる(=NEW領域のサイズを拡大する)。
  • OLD領域の最大値を、OLD領域の最小値が70%程度となるまで下げる(=NEW領域のサイズを拡大する)。

NEW領域は、物理メモリが利用できる範囲内ではあるが、以下のようになるまで拡大する必要がある。

  • 高負荷時間帯に、OLD領域のメモリ使用量が増加しない、もしくは増加しても軽微である。
  • 高負荷時間帯に、NEW領域のGCが、概ね10秒以上の間隔で実行されている(まれに連続実行されることがあるのは問題ではない)。

上記は、OLD領域が、十分に確保できることが大前提となる。OLD領域が枯渇すると、OutOfMemoryErrorが発生してしまうためだ。以下のように振舞うようになるまで、OLD領域の適正なサイズを探っていく必要がある。

  • OLD領域の拡大・縮小が発生しない、もしくは発生してもまれである。
  • OLD領域のGCが、ピーク時間帯に発生しない、もしくは発生しても発生間隔が5分間以上である。

最終的に、ピーク時の、単位時間当たりに占めるGC実行時間の割合が、4%以下となることを目標に、チューニングすることが望ましい。繰り返しになるが、OLD領域を必要十分以上に大きくしてしまわないこと、NEW領域を十分に多く確保することが望ましい。

昔は、利用可能なメモリサイズが小さかったため、OLD領域を大きく取ることが推奨されていた。これは、メモリを十分に確保できない状況で、マーク&スイープ型GCが主流であったことの名残でもある。しかし、複雑で扱うデータ量も大きなアプリで、トランザクション実行時間が秒単位になるものが混ざっているような環境においては、 OLD領域を必要十分なだけ確保した上で、NEW領域を大きく確保する必要があるので、業務システム利用における多くの場合は、OLD領域よりもNEW領域の方がだいぶ大きくなるはずだ。

マーク&スイープGCから世代別GCへの移行

2GB程度までのJavaヒープサイズの場合は、世代別GCよりも、マーク&スイープ型の方が有利である可能性がある。この傾向は、サイズが小さくなるほど顕著になる。

サービス提供開始時点ではマーク&スイープ型だったとしても、徐々に処理量が増加し、GC負荷が高騰したために、Javaヒープサイズを拡大する要求が生じて、2GBを超えるようになったら、世代別GCへ移行することを検討することが望ましい。

GC方式の変更は、パフォーマンスに対して少なくない影響を与えることから、本業務サービス提供環境では、保守的な態度で、徐々に変更していくことが望ましい。

まず、最も保守的な方針は、マーク&スイープ型GCのときのJavaヒープサイズの最大・最小の設定値を、世代別GCのOLD領域の最大・最小値にして、NEW領域を単純に追加することだ。メモリに余裕があれば、まずは、OLD領域の最小値程度(=元のJavaヒープサイズの最小値≒最大値の70%程度)を、NEW領域としてJavaヒープに追加することが望ましい。

NEW領域を追加することで、OLD領域のGC実行頻度は減少するはずなので、その後の稼働状況を一定期間評価することで、OLD領域のGC後使用量を確認し、当該の量が70%程度となるまでOLD領域サイズを縮小し、その分、NEW領域を拡大すれば良い。本業務サービス提供環境で極端な設定変更が好ましくない場合は、数回に分けて徐々に割り当てサイズを変化させていくことが望ましい。

前項の繰り返しになるが、最終的に、以下のようになればよい。なお、強大なオブジェクトがアロケーションされた場合に、連続領域が確保できないなどの理由で、GCが連続実行されることがあるが、稀であれば問題ではない。

  • OLD領域の拡大と縮小が頻繁に発生しない。(特にオンラインのピーク時には発生しない。)
  • OLD領域のGC後の使用率が70%程度である。(低すぎも、高すぎもしない。)
  • OLD領域のGC実行間隔が概ね5分間以上である。(一時間に数回程度が望ましい。)
  • NEW領域のGC実行間隔が概ね10秒以上である。
  • GC実行時間比率が4%以下である。

チューニングが必要な場合

例として、以下のようになっている場合は、チューニングが必要だ。

事象 改善方法
OLD領域が、ピーク時間帯に徐々に拡大していく。 Javaヒープサイズの拡大は負荷が高くGC実行時間も長くかかってしまうので、ピーク時間帯に必要なサイズまで、OLD領域の最小値を拡大する。但し、バッチ処理などで、徐々に拡大するのではなく一気に拡大し、処理終了後に一気に縮小する場合は、この限りではない。
OLD領域のGCが、ピーク時間帯に何度も実行されており、GC後のOLD領域の使用率が低い(50%未満である)。 NEW領域で消滅すべきオブジェクトがOLD領域に昇格してしまっているので、OLD領域のサイズを削ってNEW領域を拡大すべき。
OLD領域のGCに数秒以上かかっており、GC後の使用率が低い(50%未満である)。 OLD領域のGCは、コンカレント・マーク&スイープ型GCであり、サイズが大きくなるとそれだけGC実行時間も長くなる。GC後の使用率が70%程度になるまで、OLD領域のサイズを縮小すべき。
OLD領域がいっぱいになっていないのに、OLD領域のGC (フルGC)が発生している。 アプリケーションで、System.gc()が実行されていないか確認し、実行されていないことを確認できたら、ダイレクトバイトバッファ枯渇を疑う必要がある。

チューニングポイントは他にもたくさんある。例えば、スタックサイズ、ラージオブジェクトエリアサイズ、ダイレクトバイトバッファサイズなどは、チューニングする必要に迫られる可能性がある。また、用途によってはシステムプロパティ(-Dproperty=value)で、タイムアウト値や機能無効化などを設定する必要があるかもしれない。これらのチューニング要否を含めて、全体としてはアプリケーションの動作特性に依存して個別的に評価・判断すべきであるが、「NEWを大きく、OLDを小さく」、「サイズの差はOLDでつけて、NEWのサイズは固定」、「物理メモリがあまっていても、全体サイズを大きくしすぎない」ということは、ほとんどの場合は守るべき事柄だ。

| 日記 | comments(0) | trackbacks(0) |

スポンサーサイト

| - | - | - |
コメント



この記事のトラックバックURL
http://msugai.jugem.jp/trackback/1082
トラックバック
CALENDAR
Sun M T W T F Sat
 123456
78910111213
14151617181920
21222324252627
28293031   
<< May 2017 >>
WATCH
SEARCH THIS BLOG
LATEST ENTRIES
CATEGORIES
SELECTED ENTRIES
RECENT COMMENTS
RECENT TRACKBACKS
Twitter
MY WEB SITES
PROFILE
ADMIN
MOBILE
qrcode
ARCHIVES
SPONSORED LINKS