Java のメモリ管理

Note

Azure Spring Apps は、Azure Spring Cloud サービスの新しい名前です。 サービスの名前は新しくなりましたが、スクリーンショット、ビデオ、図などの資産の更新に取り組んでいる間、場所によってはしばらく古い名前が表示されます。

この記事の適用対象: ✔️ Basic または Standard ✔️ Enterprise

この記事では、Java のメモリ管理に関するさまざまな概念について説明します。Azure Spring Apps でホストされる Java アプリケーションの動作を理解するうえで役立ちます。

Java のメモリ モデル

Java アプリケーションのメモリにはいくつかの領域があり、それらの領域を区別する方法も 1 つではありません。 この記事では、ヒープ メモリ、非ヒープ メモリ、ダイレクト メモリに区別された Java メモリについて説明します。

ヒープ メモリ

クラスのすべてのインスタンスと配列はヒープ メモリに格納されます。 Java 仮想マシン (JVM) にはそれぞれヒープ領域が 1 つだけ存在し、それがスレッド間で共有されます。

ヒープ メモリの値は Spring Boot アクチュエータで観察できます。 Spring Boot アクチュエータは、jvm.memory.used/committed/max の一部としてヒープの値を取得します。 詳細については、「メモリの問題をトラブルシューティングするためのツール」の「jvm.memory.used/committed/max」セクションを参照してください。

ヒープ メモリは、"若い (Young) 世代" と "古い (Old) 世代" に分けられます。 以下、これらの用語について、関連する用語と共に説明します。

  • "Young 世代":すべての新しいオブジェクトはまず Young 世代に割り当てられ、世代を重ねていきます。

    • "Eden 空間": 新しいオブジェクトは Eden 空間に割り当てられます。
    • "Survivor 空間": オブジェクトはガベージ コレクションの 1 サイクルを経た後、Eden から Survivor 空間に移動されます。 Survivor 空間は、s1 と s2 の 2 つの領域に分けられます。
  • "Old 世代": "Tenured 空間" とも呼ばれます。 長時間 Survivor 空間に残っているオブジェクトは Old 世代に移動されます。

Java 8 未満では、ヒープに "Permanent 世代" と呼ばれる別のセクションも存在していました。 Java 8 以降では、Permanent 世代が、非ヒープ メモリ内のメタスペースに置き換えられています。

非ヒープ メモリ

非ヒープ メモリは次の領域に分かれています。

  • 非ヒープ メモリのうち、Java 8 以降で Permanent 世代 (permGen) の後継となった領域。 Spring Boot アクチュエータは、このセクションを観察し、それを jvm.memory.used/committed/max の一部として取得します。 つまり、jvm.memory.used/committed/max は、かつて permGen と呼ばれていた非ヒープ メモリ領域とヒープ メモリとの合計です。 以前の Permanent 世代は、次の領域から成ります。

    • "メタスペース": クラス ローダーによって読み込まれたクラス定義を格納します。
    • "圧縮クラス空間": 圧縮されたクラス ポインターに使用されます。
    • "コード キャッシュ": JIT によってコンパイルされたネイティブ コードが格納されます。
  • その他のメモリ (スレッド スタックなど)。この領域は Spring Boot アクチュエータによって観察されません。

ダイレクト メモリ

ダイレクト メモリは、nio や gzip などのサード パーティのライブラリで使用される java.nio.DirectByteBuffer によって割り当てられるメモリです。

ダイレクト メモリの値は、Spring Boot アクチュエータでは観察されません。

次の図は、前のセクションで説明した Java メモリ モデルをまとめたものです。

Java メモリ モデルの図。

Java ガベージ コレクション

Java ガベージ コレクション (GC) には、関連する用語として "Minor GC"、"Major GC"、"Full GC" の 3 つ があります。 JVM 仕様では、これらの用語が明確には定義されていません。 ここでは、"Major GC" と "Full GC" を同等と見なします。

Minor GC は、Eden 空間がいっぱいになると実行されます。 Young 世代の無効なオブジェクトはすべて削除され、有効なオブジェクトは Eden 空間から Survivor 空間の s1 へ、または s1 から s2 へと移動されます。

Full GC (Major GC) では、ヒープ全体のガベージ コレクションが実行されます。 メタスペースやダイレクト メモリなどの領域も Full GC で回収されます。これらの領域をクリーンできるのは Full GC だけです。

ヒープの最大サイズは、Minor GC と Full GC の頻度に影響します。 メタスペースとダイレクト メモリの最大サイズは、Full GC に影響します。

最大ヒープ サイズの値を小さくするとガベージ コレクションの頻度が増え、アプリはやや低速化しますが、メモリ使用量はより適切に制限されます。 最大ヒープ サイズを大きくすると、ガベージ コレクションの頻度が減り、メモリ不足 (OOM) のリスクが発生しやすくなります。 詳細については、「メモリ不足の問題によって引き起こされるアプリの再起動の問題」の「メモリ不足の問題の種類」セクションを参照してください。

メタスペースとダイレクト メモリは、Full GC によってのみ回収できます。 Full GC は、メタスペースまたはダイレクト メモリがいっぱいになると実行されます。

Java のメモリ構成

以降のセクションでは、Java のメモリ構成の重要なポイントについて説明します。

Java のコンテナー化

Azure Spring Apps のアプリケーションは、コンテナー環境で実行されます。 詳細については、「Java アプリケーションをコンテナー化する」を参照してください。

重要な JVM オプション

メモリの各領域の最大サイズは、JVM オプションを使用して構成できます。 JVM オプションは、Azure CLI コマンドまたは Azure portal を使用して設定できます。 詳細については、「メモリの問題をトラブルシューティングするためのツール」の「構成変更によって問題を解決する」セクションを参照してください。

以下に示したのは、JVM の一連のオプションとその説明です。

  • ヒープ サイズの構成

    • -Xms: 初期ヒープ サイズを絶対値で設定します。
    • -Xmx: 最大ヒープ サイズを絶対値で設定します。
    • -XX:InitialRAMPercentage: アプリ メモリ サイズに対するヒープ サイズの割合で初期ヒープ サイズを設定します。
    • -XX:MaxRAMPercentage: アプリ メモリ サイズに対するヒープ サイズの割合で最大ヒープ サイズを設定します。
  • ダイレクト メモリ サイズの構成

    • -XX:MaxDirectMemorySize: ダイレクト メモリの最大サイズを絶対値で設定します。 詳細については、Oracle のドキュメントで MaxDirectMemorySize に関するセクションを参照してください。
  • メタスペース サイズの構成

    • -XX:MaxMetaspaceSize: メタスペースの最大サイズを絶対値で設定します。

既定の最大メモリ サイズ

以降のセクションでは、既定の最大メモリ サイズがどのように設定されるかについて説明します。

既定の最大ヒープ サイズ

Azure Spring Apps では、Java アプリのヒープ メモリの既定の最大サイズが、アプリ メモリの約 50% から 80% に設定されます。 具体的には、次の設定が Azure Spring Apps によって使用されます。

  • アプリ メモリが 1 GB 未満の場合、既定の最大ヒープ サイズはアプリ メモリの 50% になります。
  • アプリ メモリが 1 GB 以上 2 GB 未満の場合、既定の最大ヒープ サイズはアプリ メモリの 60% になります。
  • アプリ メモリが 2 GB 以上 3 GB 未満の場合、既定の最大ヒープ サイズはアプリ メモリの 70% になります。
  • アプリ メモリが 3 GB 以上の場合、既定の最大ヒープ サイズはアプリ メモリの 80% になります。

ダイレクト メモリの既定の最大サイズ

ダイレクト メモリの最大サイズは、JVM オプションを使用して設定されなかった場合、JVM によって、Runtime.getRuntime.maxMemory() から返される値に自動的に設定されます。 この値は、最大ヒープ メモリ サイズとほぼ同じです。 詳細については、JDK 8 VM.java ファイルを参照してください。

メモリ使用量レイアウト

ヒープ サイズは、スループットの影響を受けます。 基本的に、構成時には、既定の最大ヒープ サイズを維持できるため、他の領域に対しては妥当なメモリが残ります。

メタスペースのサイズは、クラスの数など、コードの複雑さによって異なります。

ダイレクト メモリのサイズは、スループットと、nio や gzip などのサード パーティ製ライブラリの使用によって異なります。

以下に記載したのは、2 GB のアプリにおける標準的なメモリ レイアウトの例です。 実際にメモリ サイズの設定を構成する際の参考にしてください。

  • 合計メモリ (2,048M)
  • ヒープ メモリ: Xmx は 1433.6 M (合計メモリの 70%)。 1 日あたりのメモリ使用量の基準値は 1,200 M です。
    • Young 世代
      • Survivor 空間 (S0、S1)
      • Eden 空間
    • Old 世代
  • 非ヒープ メモリ
    • 観測領域 (Spring Boot アクチュエータによって観察される領域)
      • メタスペース: 1 日あたりの使用量の基準値は 50 M から 256 M
      • コード キャッシュ
      • 圧縮クラス空間
    • 非観測領域 (Spring Boot アクチュエータで観察されない領域): 1 日あたりの使用量の基準値は 150 M から 250 M
      • スレッドのスタック
      • GC、内部シンボル、その他
  • ダイレクト メモリ: 1 日あたりの使用量の基準値は 10 M から 200 M。

以下に同じ情報を図で示します。 灰色の数値は、1 日あたりのメモリ使用量の基準値です。

2 GB のアプリにおける標準的なメモリ レイアウトの図。

全体として、最大メモリ サイズを構成するときは、メモリ内の各領域の使用量を考慮する必要があります。また、すべての最大サイズの合計が、使用可能なメモリの合計を超えないようにしてください。

Java OOM

OOM は、アプリケーションがメモリ不足であることを意味します。 コンテナー OOM と JVM OOM という 2 つの異なる概念があります。 詳細については、「メモリ不足の問題によって引き起こされるアプリの再起動の問題」を参照してください。

関連項目