イントロ
今回は、Javaの性能に関連するトピックを紹介していきたいと思います。意外とメモリ管理の仕組みが複雑で、性能問題が発生した時に手を焼くことがあると思いますので、Javaのメモリ管理の仕組みを理解しておくことが重要です。JVM(Java Virtual Machine)、Javaのメモリ空間の使い方、ガベージコレクション(GC)についてまとめていますので、ぜひ参考にしてみて下さい。
JVM(Java Virtual Machine)とは
JVMはJavaプログラムを動かすために必要なソフトウェアとなります。Javaの思想として、作ったプログラムをOSに依存せずに動作させることができるというものがあります。そのためにOSに依存しないJavaバイトコードと呼ばれるプログラムを各OS環境に配布するのですが、JavaバイトコードはOS上で直接実行することができません。そこで各OSにインストールされているJVMがJavaバイトコードを実行環境に適した形式(ネイティブコード)に変換することで、異なるOS上でJavaプログラムが実行できる方式となっております。
Java(JVM)のメモリ空間の使い方
JVMが使用するメモリ空間の内訳は下記の通りとなります。後続のガベージコレクションの仕組みやチューニング方法を理解するために抑えておく必要がある情報になりますので、ぜひご参照ください。
Java Heap | Javaプログラムが利用するメモリ領域であり、Javaオブジェクトが格納されます。New領域とOld領域で構成されます。 |
New | 寿命の短いJavaオブジェクトを格納します。通常、Javaアプリケーションで要求されたオブジェクトは、このNew世代領域に生成されます。Young領域とも呼ばれます。 |
Eden | 新しいJavaオブジェクトが配置された際に最初に配置されるメモリ領域です。 |
Survivor | Eden領域がいっぱいとなりメモリが足りなくなるとScavenge GC (New領域を対象としたGC。Minor GCとも言う。)が発生します。Scavenge GCが走ったタイミングで使用中のJavaオブジェクトはSurvivor領域に移動されます。 |
From/To | Eden領域からScavenge GCによって移動されるメモリ領域となります。ToからFromに移動させることもあります。GCが走るたびにFromとToの役割が入れ替わることが特徴です。Survivor0、Survivor1と呼んだりもします。 |
Old | Scavenge GCが発生するたびに、New領域にあるJavaオブジェクトのデータに対してScavenge GCの回数が記録されます。「Max Tenuring Threshold」にて設定されている回数を超えた際に、New領域からOld領域にJavaオブジェクトが移動されます。Old領域はTenured領域とも呼ばれています。 |
Native Memory | JVMが内部処理のために使用しているメモリです。 |
Metaspace | ロードされたclassやメソッドなどの情報が格納されるメモリ領域となります。Java8以前ではPermanent領域と呼ばれていた領域です。 |
C Heap | JVM自身が使用するメモリ領域です。 |
Thread Stack | プログラムがスレッドを生成した際に、OSがそのスレッドに対して割り当てるメモリ領域です。 |
ガベージコレクション(GC)
JVMが使用しているメモリ領域の概要が分かったところで、ガベージコレクション(GC)についての理解を深めていきましょう。Javaの性能に影響を与える要素としてまず思いつくのはこのGCだと思いますので、挙動を理解しておくことは必須です。JavaにおけるGCとは、Javaオブジェクトが確保していたメモリ領域の開放をする処理となります。JavaにおいてはCやC++のようにメモリ領域の開放をプログラムコードに記載する必要はなく、JVMが自動的にGC処理を実行する仕組みになっています。GCにNew領域のメモリを開放するScavenge GCとNew領域に加えてOld領域まで解放対象とするFull GCという処理があります。Scavenge GCが数msから数十msで処理が終わるので10秒おき程度で発生する分にはアプリケーションの動作には影響しない一方で、FullGCは処理に数秒かかるうえにStop the Worldというすべてのアプリケーションスレッドが停止する事象が起きてアプリケーションの動作に影響が出てしまうことがあります。したがってJVMで利用するメモリ領域の値を適切にチューニングすることで、FullGCの間隔を1時間に5回から10回程度に抑えることが重要になってきます。
Scavenge GC
Scavenge GCの処理対象は、New領域に存在するJavaオブジェクトとなります。Eden領域がいっぱいになった時に起動し、Eden領域とSurvivor領域における不要となったJavaオブジェクトを破棄し、使用されているオブジェクトを移動させる処理を行います。移動の種類としてはEden領域からTo領域への移動、From領域とTo領域間の移動、From領域/To領域からOld領域への移動の3種類となります。
まず、Eden領域がいっぱいになったタイミングでScavenge GCが走ります。その際に、Edenに格納されている不要となったJavaオブジェクトが利用しているメモリ領域は解放されます。一方でまだ使用されているJavaオブジェクトはEden領域からOld領域に移動されます。
次にEden領域がいっぱいになった場合は同様にScavenge GCが走り、Edenに格納されている不要となったJavaオブジェクトが利用しているメモリ領域が解放されます。この際、使用されているJavaオブジェクトがEdenからTo領域ではなく、From領域に移動することが上記との差分となります。また、もともとToに格納されてたJavaオブジェクトについては、使用しなくなったものは解放されますが、まだ使用中のものはTo領域からFrom領域に移動されます。
その後はEden領域がいっぱいになってScavenge GCが走った際に、メモリ領域の解放およびFrom領域とTo領域間のJavaオブジェクトの移動が繰り返されます。この移動回数が「MaxTenuringThreshold」と呼ばれる閾値を上回ったオブジェクトについては、New領域からOld領域に移動されることになります。この値が小さいとOldに移動するJavaオブジェクトの数が増えてきて後述するFullGCの頻度も増えてしまうので、適切な値にチューニングするようにしましょう。
Full GC
Full GCの処理対象領域はNew領域に加えて、Old領域およびMetaspace領域が対象となります。Full GCが発生するタイミングはいろいろあるのですが、ざっくりOld領域へのJavaオブジェクト移動によってOld領域が枯渇した際にFull GCが発生すると理解しておいてもらえれば良いかと思います。
Full GCの発生頻度を抑えるためには、各種Javaメモリ領域の容量をチューニングしたり、アプリケーションを回収して古いJavaオブジェクトを使用しないようにする必要があります。少し込み入った話になりますので具体的なチューニング方法については別の記事で紹介しようと思います。
まとめ
- JavaはJVMと呼ばれるソフトウェア上で動作させており、異なるOS上で動作させることができる
- Javaが利用するメモリ空間は大きくJavaヒープ、ネイティブメモリがある。Javaオブジェクトが使用するのはJavaヒープ領域であり、さらに細かく分けられたメモリ空間のなかでJavaオブジェクトが管理されている。
- 使われなくなったJavaオブジェクトはGCが起きたタイミングでメモリ解放される。
- GCにはScavenge GCとFull GCという種類が存在し、性能影響を与えないようにFull GCの回数を抑える必要がある。
ここまで読んでいただきありがとうございました。ほかにも性能関連の記事をいくつか記載してますので、ぜひ合わせて読んでもらえると嬉しいです。
【Apache】並列処理の仕組みとチューニング方法をまとめてみた。
【Tomcat】コネクションプールのチューニングによる流量制限の方法をまとめてみた。
【Tomcat】並列処理の仕組みと流量制限のためのチューニング方法
参考資料
今回の記事を記載するにあたり参考にした資料を記載します。
https://www.scientecheasy.com/2021/03/java-bytecode.html/
https://www.hitachi.co.jp/Prod/comp/soft1/cosminexus/apserver/document/download/wp_fullgc.pdf
https://atmarkit.itmedia.co.jp/ait/articles/0504/02/news005.html
コメント