JVMパフォーマンスチューニング：チューニングとは何か、適切なプランの立て方について

![Something went wrong]()
**JVM**のパフォーマンスチューニングは、体系的で複雑な作業です。この記事では、その概念を説明し、**JVM**のパラメータを使用してアプリケーションのチューニングを実行する方法を示します。

**本ブログは英語版からの翻訳です。オリジナルは[こちら](https://www.alibabacloud.com/blog/how-to-properly-plan-jvm-performance-tuning_594663)からご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。**

#パフォーマンスチューニングレイヤー
システムの性能を向上させるためには、様々な視点やレイヤーから最適化する必要があります。最適化の対象となるレイヤーは以下の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/c760a744-75b1-f0c4-cdb0-fda2a18b7a81.png)



図に示すように、JVMのチューニング以外にも、多くの層で最適化が必要です。システムのチューニングは、JVMのチューニングだけではありません。むしろ、システムの性能を向上させるためには、システム全体のチューニングが必要です。この記事では、JVMのチューニングについてのみ説明します。他のチューニングの側面については後述します。

JVMチューニングの前に、プロジェクトのアーキテクチャとコードがチューニングされているか、現在のプロジェクトに最適なアーキテクチャとコードであると仮定します。この2つの仮定はJVMチューニングの基本であり、アーキテクチャのチューニングはシステム性能に最も大きな影響を与えます。アーキテクチャに欠陥があったり、JVMチューニングだけを実行してコード最適化を必要とするアプリケーションからの質的な飛躍を期待することはできません。

さらに、チューニングを開始する前に、明確なパフォーマンス最適化の目標を持ち、現在のパフォーマンスのボトルネックを把握しておく必要があります。ボトルネックを最適化するためには、アプリケーション上でストレステストやベンチマークテストを実施し、最適化されたアプリケーションが目的の目標を満たしているかどうかを確認するために、さまざまなモニタリングや統計ツールを使用する必要があります。

#JVMチューニングの手順
チューニングの最終的な目標は、アプリケーションをハードウェア消費の最小コストでより大きなスループットを持つようにすることです。JVMのチューニングも例外ではありません。JVMのチューニングは、主にガベージコレクタを最適化して収集性能を向上させることで、VM上で実行されるアプリケーションが、より少ないメモリ使用量でより低いレイテンシを経験しながら、より大きなスループットを実現できるようにします。メモリ使用量が少ない／レイテンシが少ないほどパフォーマンスが良いというわけではないことに注意してください。最適な選択をすることが重要なのです。

###パフォーマンスメトリクス
パフォーマンスのボトルネックを見つけて評価するためには、パフォーマンスメトリクスの定義を知っておく必要があります。JVMチューニングのためには、以下の3つの定義を知り、これらのメトリクスを評価のベースとして使用する必要があります。

- スループット：重要なメトリクスの一つです。スループットとは、ガベージコレクタがアプリケーションが達成できる最高のパフォーマンスを指し、ガベージコレクションによる一時停止時間やメモリ消費を考慮しません。
- レイテンシ：レイテンシは、実行中のプロセス中にアプリケーションの振動を避けるために、ガベージコレクションの結果として生じる一時停止時間がどれだけ減るかを測定します。
- メモリ使用量：ガベージコレクタがスムーズに動作するために必要なメモリの量を指します。
3 つの属性のいずれかのパフォーマンスの向上は、他の 1 つまたは 2 つの属性のパフォーマンスの損失をほぼ犠牲にしている。アプリケーションのビジネス要件は、1 つまたは 2 つの属性がアプリケーションにとってどれだけ重要かを決定します。

###パフォーマンスチューニングの原則
チューニングプロセスの間、以下の3つの原則は、所望のアプリケーションのパフォーマンス要件を満たすために、より簡単なガベージコレクションのチューニングを実装するのに役立ちます。

- マイナーGC収集の原則：毎回マイナーGCは、アプリケーションのためのフルGCの頻度を減らすために、できるだけ多くのガベージオブジェクトを収集する必要があります。
- GCメモリ最大化の原則：スループットとレイテンシの問題を解決する場合、ガベージコレクタが使用するメモリが大きければ大きいほど、ガベージコレクションがより効率的になり、アプリケーションがよりスムーズになります。
- GCチューニング「3つのうち2つ」の原則：スループット、レイテンシ、メモリ使用量の3つの属性すべてをチューニングするのではなく、3つの性能属性のうち2つだけをチューニングすべきです。

###パフォーマンスチューニングの手順
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/8cef60ec-19d9-3a0b-64b0-8f2a7a7d5161.png)



前述の図は、アプリケーションの基本的なJVMチューニングの手順を示しています。JVMチューニングは、継続的な構成最適化と、性能テスト結果に基づく複数回の反復を含むことがわかります。各所望のシステムメトリックが満たされる前に、前のステップのそれぞれが複数回の反復を経験することがあります。場合によっては、特定のメトリックを満たすために、以前のパラメータを何度もチューニングする必要があり、以前のすべてのステップを再度テストする必要があります。

さらに、チューニングは一般的に、アプリケーションのメモリ使用量の要件を満たすことから始まり、次にレイテンシとスループットが必要になります。チューニングは、この一連のステップに従うべきです。これらのチューニングステップの順序を逆にすることはできません。以下のセクションでは、例を用いて各チューニングステップを詳しく説明します。

JVMを実行するために、JDK 1.6以降で公式に推奨されているサーバーモードを直接選択します。

ガベージコレクタとしてJDK 1.6-1.8のデフォルトのパラレルコレクタを使用します。(若い世代にはparallelGC、古い世代にはparallelOldGCを使用します)。

#メモリ使用量の決定
メモリ使用量を決定する前に、2つのことを知っておく必要があります。

1、アプリケーションの動作フェーズ
2、ＪＶＭメモリ割り当て

###操作フェーズ
アプリケーションの動作を以下の3つのフェーズに分けて説明します。

- 初期化：JVMはアプリケーションをロードし、アプリケーションのメインモジュールとデータを初期化します。
- 安定化：アプリケーションが長時間動作していて、ストレステストを受けている状態。各パフォーマンスパラメータは安定状態にあります。コア機能が実行され、JITコンパイルを利用してウォーミングアップされています。
- まとめ：最終的なまとめフェーズでは、対応するレポートを生成するためにいくつかのベンチマークテストを実施しています。このフェーズでは特に注意を払う必要はありません。
メモリ使用量やアクティブデータのサイズは、プロジェクトの立ち上げ段階ではなく、アプリケーションの安定化段階で決定する必要があります。メモリ使用量の決定方法を説明する前に、まずJVMのメモリ割り当てを見てみましょう。

###JVMメモリの割り当てとパラメータ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/571085a4-1a89-6ce2-8c9d-dc8480ca0d2c.png)

主なJVMヒープ空間は、若い世代、古い世代、および永続的な世代で構成されています。若い世代のサイズ、古い世代のサイズ、および永続的な世代のサイズが合計のヒープサイズを構成します。特定のオブジェクトの推進方法については、ここでは説明しません。それでは、以下のJVMコマンドがヒープサイズをどのように指定するかを見てみましょう。以下のパラメータがヒープサイズの指定に使用されていない場合、仮想マシンは自動的に適切な値を選択しますが、これはシステムのオーバーヘッドに基づいて自動的に調整される可能性があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/13164e04-ca22-c0e2-6699-36d55bdf3f50.png)

パフォーマンスのオーバーヘッドが懸念される場合は、パーマネント世代のサイズ調整を実装できるのは FullGC のみなので、可能な限り初期サイズとパーマネント世代の最大サイズを同じ値に設定してください。

###アクティブデータのサイズを計算
アクティブデータのサイズを計算するには、以下の手順に従います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/807fe56f-ad75-97bb-5efa-88a516846b22.png)

前述したように、アクティブ・データ・サイズは、アプリケーションの安定性フェーズが始まってから長い間アクティブな状態にあるデータがJavaヒープの空間をどれだけ占有しているかによって測定する必要があります。

アクティブ・データ・サイズを計算する際には、以下の要件を必ず満たすようにしてください。

- 起動パラメータを手動で設定するのではなく、テストを実行する際にデフォルトのJVMパラメータを使用してください。
- Full GC 2011 が発生したときに、アプリケーションが安定状態にあることを確認してください。
デフォルトの JVM 起動パラメータを使用するのは、アプリケーションが安定フェーズにあるときに必要なメモリ使用量を観察するためです。

###アプリケーションが安定フェーズにあるのはいつですか？
十分なストレスがかかった後、アプリケーションは、本番環境でビジネスのピーク時にビジネス要件を満たすワークロードに到達し、ピークに到達した後も安定した状態を維持している場合にのみ、安定フェーズにあります。したがって、アプリケーションが安定フェーズに達したかどうかを判断するためには、ストレステストが不可欠です。アプリケーションのストレステストをどのように実施するかは、この記事の範囲内ではありません。この質問については、後で別の記事で説明します。

アプリケーションが安定段階にあると判断した後、アプリケーションの GC ログ、特に Full GC ログに注意を払ってください。

```
GC log directive: -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:<filename>
```

最適化に必要な情報を収集するにはGCログが最適です。本番環境でもGCログを有効にすることで、問題点を見つけ出すことができます。GCログを有効にすることで、豊富なデータを提供しながらパフォーマンスへの影響を最小限に抑えることができます。

FullGCログが必要です。FullGログが利用できない場合は、監視ツールを使用して呼び出しを強制するか、以下のコマンドを使用してログをトリガーします。

```
jmap -histo:live pid
```

安定期にフルGCが発動した場合、以下の情報を得ることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/8b8747a8-8592-57d5-6d55-e6ebd76b12e1.png)

前述のGCログから、フルGC中のアプリケーション全体のヒープ使用量とGC時間を大まかに見積もることができます。より正確な推定値を得るためには、複数回情報を収集して平均値を求めます。または、最長の FullGC を使用して推定することもできます。

上図では、フルGC後、old generationスペースの93168KB(約93MB)が占有されています。このデータ量を旧世代空間のアクティブデータとみなします。

その他のヒープ空間は、以下のルールで割り当てられます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/6b1268f5-b1a8-b6ad-005f-abe20d43cd3e.png)

前述のルールと前図のFullGC情報から、アプリケーションのヒープ空間は以下のように計画することができます。

Javaのヒープ空間：373MB＝93168KB（old generationスペース）×4

Young generationスペース：140MB＝93168KB（old generationスペース）×1.5

Permanent generationスペース：５ＭＢ＝３１３５ＫＢ（Permanent generationスペース）×１．

old generationスペース：233MB＝373MB（ヒープ空間）-140MB（Young generationスペース）

対応するアプリケーション起動パラメータは

```
java -Xms373m -Xmx373m -Xmn140m -XX:PermSize=5m -XX:MaxPermSize=5m
```


#レイテンシチューニング
アプリケーションのアクティブなデータサイズを決定した後、レイテンシのチューニングを行う必要があります。この時点では、ヒープメモリサイズとレイテンシがアプリケーションの要件を満たすことができないため、アプリケーションの実際の要件に基づいてアプリケーションをデバッグする必要があります。

このフェーズでは、ヒープサイズ構成を再度最適化し、GC の継続時間と頻度を評価し、別のガベージコレクタに切り替える必要があるかどうかを決定する必要があるかもしれません。

###システムのレイテンシ要件
チューニングを行う前に、システムのレイテンシ要件が何であるか、そしてどのメトリックがレイテンシのためにチューニングできるかを知る必要があります。

- アプリケーションの許容可能な平均ダウンタイム：この時間は、測定されたマイナー GC 2011 の継続時間と比較されます。
- 許容可能なマイナー GC 2011 の周波数：マイナー GC 2011 の周波数は、許容値と比較されます。
- 許容可能な最大休止時間：最大一時停止時間：最大一時停止時間は、最悪の場合の FullGC 持続時間と比較されます。
- 許容可能な最大一時停止の発生頻度：これは基本的にFullGCの発生頻度です。

前述のメトリクスの中でも、平均ダウンタイムと最大一時停止時間には特に注意を払ってください。この2つのメトリクスは、ユーザーエクスペリエンスにとって非常に重要です。

前述の要件に基づき、以下のデータを取得する必要があります。

- マイナーＧＣの持続時間
- マイナーGCの数
- FullGCの最長持続時間
- 最悪の場合のFullGC周波数

###Young Generationのサイズを最適化
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/04090a56-78dd-a077-b460-069aceae7fe5.png)

例えば、先行するGCログでは、マイナーGCの平均継続時間は0.069秒であり、マイナーGCは0.389秒に1回発生しています。

平均ダウンタイムが50msに設定されており、現在の持続時間（69ms）は明らかに長すぎて調整が必要です。

**young generation**のスペースが大きくなればなるほど、MinorGCの持続時間は長くなり、頻度は低くなることがわかっています。

持続時間を短くするには、young generationスペースのサイズを小さくする必要があります。

周波数を下げるためには、young generationスペースのサイズを大きくする必要があります。

young generation空間のサイズ変更による他のセクションへの影響を最小限に抑えるために、young generationスペースのサイズを変更する際には、可能であればold generationスペースのサイズのままにしておきます。

例えば、young generationスペースのサイズを10%縮小した場合、old generationスペースとpermanent generationスペースのサイズは変更しないようにします。このステップでの最適化後のパラメータは以下の通りです。

```
java -Xms359m -Xmx359m -Xmn126m -XX:PermSize=5m -XX:MaxPermSize=5m

The size of the young generation is changed from 140 MB to 126 MB; the heap size is changed accordingly; the old generation has no changes at this point.
```
###**Old Generation**のサイズを最適化する
前のステップと同様に、最適化の前にGCログからいくつかのデータを取得する必要があります。このステップでは、FullGCの継続時間と頻度に焦点を当てます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/42abc293-9e59-16d8-8ae3-f0bcf22a1fe4.png)




前図から以下の情報を得ることができます。

```
The average FullGC frequency is 1 FullGC every 5.8s.

The average FullGC duration is 0.14s.

(This is only a test. FullGC lasts longer in real projects.)
```

###オブジェクト昇格率
FullGCのログがない場合、評価を行うことはできますか？昇格率を評価に利用することは可能です。

例えば、先ほどの起動パラメータでは、old generationのサイズは233MBとなっています。

この233MBの空き容量を占めるのにどれくらいの時間がかかるかは、young generationからold generationへの昇格率に依存します。

old generationの昇格使用率＝各MinorGC後のJavaヒープ使用率-MinorGC後のyoung generationの使用率

オブジェクト昇格率＝平均値（old generation利用を毎回昇格させる）／old generationスペース

オブジェクト昇格率があれば、old generationのスペースを占有するのに必要なマイナーGCの数と、1つのフルGCの大まかな持続時間を計算することができます。

例を挙げておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/dd80178d-18b2-b6ba-e871-92595104714e.png)




前述の図には、以下のように記載されています。

```
After the first minor GC, the usage of the old generation space is 8 KB (13740 KB - 13732 KB).

After the second minor GC, the usage of the old generation space is 4489 KB (22394 KB - 17905 KB).

After the third minor GC, the usage of the old generation space is 16822 KB (34739 KB - 17917 KB).

After the fourth minor GC, the usage of the old generation space is 30230 KB (48143 KB - 17913 KB).

After the fifth minor GC, the usage of the old generation space is 44195 KB (62112 KB - 17917 KB).
```

各マイナーGC後の旧世代の昇格使用法

```
Between the second and the first minorGCs: 4481 KB

Between the third and the second minorGCs: 12333 KB

Between the fourth and the third minorGCs: 13408 KB

Between the fifth and the fourth minorGCs: 13965 KB
```

算出後、以下の情報を得ることができます。

```
The average usage promotion for each minorGC is 12211 KB (about 12 MB).

In the preceding figure, the minorGC happens once every 213ms on average.

Promotion rate = 12211 KB/213ms = 57 KB/ms

It takes about 4.185s (233*1024/57 = 4185ms) to fully occupy 233 MB of the old generation space.
```

前述の2つの方法を用いて、最悪のフルGC周波数を推定することができます。旧世代のサイズを変更することで、Full GC の頻度を調整することができます。Full GC が長すぎてアプリケーションの最低遅延要件を満たすことができない場合は、ガベージコレクタを切り替える必要があります。次の記事では、異なるガベージコレクタに切り替える方法について詳しく説明します（例えば、カレントマークスイープ、CMSに切り替えるなど）。CMSのチューニングは少し違います。

#スループットのチューニング
前述のチューニングステップを経て、いよいよ最後のチューニングステップに入ります。このステップでは、前の結果に対してスループットテストを行い、微調整を行います。

スループットのチューニングは、主にアプリケーションのスループット要件に基づいて行われます。アプリケーションは、全体的なアプリケーション要件とテストから導き出される包括的なスループットメトリックを持っている必要があります。アプリケーションのスループットが期待されたスループット目標に達するか、それを超えると、チューニングを終了することができます。

最適化後もアプリケーションのスループットの目標に到達できない場合は、スループット要件を見直し、現在のスループットと目標との間のギャップがどの程度あるかを評価する必要があります。もしギャップが 20% 程度であれば、パラメータを変更してメモリを増やし、アプリケーションを再度デバッグすることができます。ギャップが大きすぎる場合は、アプリケーション全体の観点から、設計とスループットの目標が一致しているかどうかを検討し、スループットの目標を再評価する必要があります。

ガベージコレクタの場合、スループットチューニングの目標は、フルGCやStop-The-World CMSの発生を減らすか回避することです。2 つのガベージコレクタの方法のどちらも、アプリケーションのスループットの低下につながる可能性があります。オブジェクトが古い世代に急速に昇格するのを防ぐために、MinorGC フェーズで可能な限り多くのオブジェクトをリサイクルするようにしてください。

#結論
Plumbrは、84,936件の事例をもとに特定のガベージコレクタの利用状況を調査しました。ガベージコレクタが明示的に指定されているケースのうち、13%のケースでは、コンカレントマークスイープ(CMS)コレクタが最も頻繁に使用されています。しかし、これらのケースの大部分では、最適なガベージコレクタは選択されていません。この大多数のケースが約87%を占めています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/c46776ff-8974-a1a3-d005-e03d1f6070bb.png)

JVMのチューニングは、体系的で複雑な作業です。現在のところ、JVMの下での自動調整は非常に優れており、基本的な初期パラメータを設定することで、一般的なアプリケーションが安定して動作することを保証できます。一部のチームでは、アプリケーションのパフォーマンスが高い優先順位を取らない場合があります。この場合、デフォルトのガベージコレクタは通常、所望の要件を満たすのに十分です。チューニングは、あなた自身の状況に基づいて行うべきです。

*アリババクラウドは日本に２つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
[アリババクラウドジャパン公式ページ](https://www.alibabacloud.com/ja)*