Java ヒープの未使用メモリをOSに返却する


はじめに

Java ヒープは大抵の場合、未使用のメモリをOSに返却しません。 最初は小さなヒープサイズであっても、大量のメモリが必要となりヒープが拡大すると、拡大したメモリ領域が保持し続けられます。

ディスクトップアプリケーションであったり、メモリの限られるコンテナ環境(1つのコンテナに複数のプロセス)での利用時には、極力無駄なメモリを解放したいです。

例えば、大量メモリを消費する IntelliJ IDEA は、JDK をフォークした独自の JetBrains Runtime で、-XX:JbrShrinkingGcMaxHeapFreeRatio というJVMオプションを追加し、GC 後のヒープの縮小を制御できるようにして対処したりしています。

独自に JDK を拡張するまでいかずとも、通常の JVM オプションで Java ヒープの縮小を行う方法はいくつかあります。


GC オプション

Java ヒープ サイズの制御は GCによって異なります。 JDK25 時点では、以下の GC が選択できます。

オプション 説明
-XX:+UseSerialGC シリアル・ガベージ・コレクタの使用を有効
-XX:+UseParallelGC Parallel Scavengeガベージ・コレクタ(スループット・コレクタ)の使用を有効
-XX:+UseG1GC ガベージファースト・ガベージ・コレクタの使用を有効
-XX:+UseZGC Zガベージ・コレクタ(ZGC)の使用を有効
-XX:+UseShenandoahGC Shenandoahガベージ・コレクタの使用を有効(-XX:+UnlockExperimentalVMOptionsの指定が必要)
  • +UseSerialGC は、後述のオプション設定と合わせて、Full GC のタイミングでヒープサイズの縮小が行われます
  • +UseParallelGC でパラレルGCを選択した場合、(後述のオプションを設定したとしても)ヒープサイズは積極的に縮小されません
  • +UseG1GC は JDK 12 で導入された JEP 346: Promptly Return Unused Committed Memory from G1によりヒープの解放が制御可能になりました
  • +UseZGC は ZUncommit 機能を備えるため、未使用のヒープをOSに返却しやすいです
  • Shenandoah についてはここでは調査していません

+UseSerialGC をあえて利用するケースは少ないかもしれませんが、+UseG1GC +UseZGC はオーバーヘッドがあるため、ヒープ使用量が下駄を履いた状態となります。ごく小さなアプリケーションであれば、あえて +UseSerialGC を選択することで、メモリ使用量を最小限にできます。


Java ヒープサイズ

ヒープサイズの指定は、以下のオプションが用意されています。

オプション 説明
-Xms<size> ヒープの最小サイズおよび初期サイズ(バイト単位)。-XX:InitialHeapSizeと同等
-Xmx<size> ヒープの最大サイズの(バイト単位)。-XX:MaxHeapSizeと同等
-Xmn<size> Young世代の初期および最大サイズ(バイト単位)。-XX:NewSize-XX:MaxNewSize で個別指定可能

-Xms-Xmx に同じ値を設定すると、ヒープサイズが固定され、ヒープサイズの縮小は行われません。

ヒープサイズを縮小するには、-Xms のみを指定したり、-Xms-Xmx を異なる値にする必要があります。


拡張ガベージ・コレクション・オプション

ヒープサイズの縮小・拡張を行う以下のオプションがあります。

オプション 説明
-XX:MaxHeapFreeRatio=<percent> 空きヒープ領域がこの値を超えて拡大した場合、ヒープが縮小される。 デフォルト70%
-XX:MinHeapFreeRatio=<percent> 空きヒープ領域がこの値を下回った場合、ヒープが拡張される。 デフォルト40%
-XX:-ShrinkHeapInSteps Javaヒープの段階的な削減を無効化(デフォルトは有効-XX:+ShrinkHeapInSteps)。無効化すると、ヒープを目標サイズまで1回で減らす

このオプションを設定することで、ヒープのサイズと空きヒープの割合を制御することができます。

例えば、以下のように設定することで、ヒープサイズを小さく保つことができます。

-XX:MaxHeapFreeRatio=10 -XX:MinHeapFreeRatio=5 -XX:-ShrinkHeapInSteps

ただし、パラレルGC についてはこの設定をしても、ヒープの縮小は観測できませんでした。また、ZGCについては後述の別のオプションで制御することになります。


シリアルGC の未使用メモリ返却

シリアルGCでは、(極端な例ですが)以下のようにすると、ヒープサイズが直ぐに縮小されます。

-XX:+UseSerialGC -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10 -XX:-ShrinkHeapInSteps

ただし、ヒープサイズが縮小されるタイミングは、Full GC 発生のタイミングになります。逆に言えば、Full GC が発生しない限り、ヒープ領域は解放されません。 アイドル時に周期的にメモリを返却する機能はありません。

大量メモリが必要な処理の後で明示的に System.gc() を呼び出したり、GC Daemon を動かして定期的に System.gc() を発行すれば、ヒープ領域を小さく保つことができます。


パラレルGC の未使用メモリ返却

パラレルGCでは、明示的な System.gc() 呼び出しでヒープサイズが縮小されません。使用領域は解放されますが、ヒープサイズ自体の縮小は行われません。

パラレルGCでは、最大GC停止時間目標(-XX:MaxGCPauseMillis=<n>)、アプリケーションのスループット目標(-XX:GCTimeRatio=<n>)に応じて、世代サイズの調整が行われます。この統計を保持するために、System.gc() の呼び出しでヒープ自身のサイズ変更は行われないようになっている気がします。

パラレルGCで、拡大したヒープサイズを縮小させるには、GC の判断により発生する Full GC のタイミング(OLD領域から溢れるなど)を待つしかないようです。

例えば、以下のようにJVMオプションを設定することはできます。これにより、GC起点での Full GC が発生すれば、ヒープサイズはヒープ使用量近くまで縮小されます。 しかし、アイドル時に周期的にメモリを返却するような挙動にはなりません。

-XX:+UseParallelGC
-XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10
-XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90

OS によっても多少挙動が異なります。

一応、関連オプションを載せておきます。

オプション 説明
-XX:MaxGCPauseMillis=<n> 最大GC停止時間目標。GC による一時停止を n ミリ秒未満に維持するように、Java ヒープサイズおよびその他の GC 関連パラメータを調整
-XX:GCTimeRatio=<n> スループット目標。GCにかけてよい時間の割合を指定。アプリ実行時間に対するGC時間の許容比率で、1 / (1 + n) となる。19 と設定すると 5% のGC時間となる(アプリケーションはコレクタの 19 倍の時間を取得)。
-XX:+UseAdaptiveSizePolicy 適応型世代サイズ変更の使用を有効にする。デフォルトでは有効。
-XX:AdaptiveSizePolicyWeight=<prcent> ヒープサイズを自動調整する際、「過去の統計データ(平均)」と「直近のGC結果」のどちらを重視するかの重み付け(0~100)。デフォルト 10
-XX:YoungGenerationSizeIncrement=<prcent> Young世代サイズの拡大率。デフォルト20
-XX:TenuredGenerationSizeIncrement=<prcent> Tenured世代サイズの拡大率。デフォルト20
-XX:AdaptiveSizeDecrementScaleFactor=<n> 世代サイズの縮小係数。拡大率がXパーセントの場合、縮小率はX/nパーセントとなる


G1GC の未使用メモリ返却

G1GC は、full GC か concurrent cycle で Java ヒープをを解放します(この時の割合は前述の MaxHeapFreeRatioMinHeapFreeRatio で設定します)。しかし、G1GC は full GC を回避するように試み、concurrent cycle も ヒープ占有率とアロケーションアクティビティを基にトリガされるため、多くのケースでヒープは解放されません。

これに対処するため、JDK 12 で JEP 346: Promptly Return Unused Committed Memory from G1 が導入され、以下のオプションで GC の間隔を制御できるようになりました。

オプション 説明
-XX:G1PeriodicGCInterval=<millis> G1がガベージ・コレクションの実行を検討する最小間隔(ミリ秒)を指定。デフォルト0で無効化されている
-XX:G1PeriodicGCSystemLoadThreshold=<n> G1PeriodicGCInterval によるガベージ・コレクションを、システム負荷が高い場合に実行させないようにする。1分間のロードアベレージの閾値を指定
-XX:+G1PeriodicGCInvokesConcurrent 定期的なガベージ・コレクションのタイプ(コンカレントかそうではないか)を設定。設定されている場合はコンカレント・マーキングがトリガーされるか既存のコレクション・サイクルが継続する。-XX:-G1PeriodicGCInvokesConcurrent とするとフルGCがトリガーされる。

たとえば、(極端な例として)以下のようにすると、ヒープは頻繁に縮小されるようになります。

-XX:+UseG1GC -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10 -XX:-ShrinkHeapInSteps -XX:G1PeriodicGCInterval=30000 -XX:+G1PeriodicGCInvokesConcurrent


ZGC の未使用メモリ返却

ZGCはデフォルトで、未使用のメモリをコミット解除して、OSにメモリを返却します(最小ヒープ・サイズ -Xms 以下にはならない)。 この機能は、-XX:-ZUncommitを使用して無効にすることもできます。最小ヒープ・サイズ -Xms と最大ヒープ・サイズ -Xmx が等しい場合は、この機能は暗黙的に無効になります。

ZGC の未使用メモリ返却に関連するオプションは以下になります。

| オプション | 説明 | | ------------------------------ | ----------------------------------------------------------------------------------------------------------- | | -XX:+ZUncommit | ZGCの未使用ヒープ・メモリの非コミットを有効にする。 デフォルトで有効 | | -XX:ZUncommitDelay=<seconds> | メモリが未コミットになるまでの未使用期間(遅延時間)(秒)を指定。デフォルト 300 (5分)。 | | -XX:SoftMaxHeapSize=<size> | ZGCが維持しようとするヒープサイズの目安。ZGCは可能な限り速やかにSoftMaxHeapSize以下に戻り、メモリをオペレーティングシステムに返却しようと試みる。デフォルトは MaxHeapSize の値。 | 例えば、以下のようにすると、ヒープサイズを SoftMaxHeapSize に保とうと、頻繁にヒープサイズが縮小されるようになります。

-XX:+UseZGC -XX:+ZUncommit -XX:ZUncommitDelay=10 -XX:SoftMaxHeapSize=256m


ネイティブ・ヒープ の解放

Java ヒープの他、ネイティブ・ヒープの解放オプションもあります。 しかしこれは Linux のみで有効なオプションです。

オプション 説明
-XX:TrimNativeHeapInterval=<millis> JVMがネイティブ・ヒープをトリミングする間隔(ミリ秒)。glibc を持つLinuxのみ。0(デフォルト)で無効。