JVM ヒープメモリ

Java ヒープメモリ

ヒープメモリとは

ヒープ(Heap)メモリは、Java プログラムの実行中に動的に生成されたオブジェクト(new 演算子で生成されたオブジェクトまたはインスタンス)が保存される空間である。
ここに生成されたオブジェクトは、他のオブジェクトのフィールド、またはスタック上に存在する他のメソッドから参照されることがある。参照する変数がなくなると、そのオブジェクトは不要なものとみなされ、Garbage Collector によって割り当てが解除される。

ヒープメモリ領域の役割

ヒープ(Heap)メモリは、Java プログラムの実行中に動的に生成されたオブジェクト(new 演算子で生成されたオブジェクトまたはインスタンス)が保存される空間である。
ここに生成されたオブジェクトは、他のオブジェクトのフィールド、またはスタック上に存在する他のメソッドから参照されることがある。参照する変数がなくなると、そのオブジェクトは不要なものとみなされ、Garbage Collector によって割り当てが解除される。

ヒープ領域は機能が多様であるため、複数の部分に分けられる。オブジェクトが最初に生成されたときに保存される空間と、オブジェクト生成後に一定時間使用されなければ移動され、ガベージコレクションを実行できるよう分類される空間に分けて管理する。

Java 7 HotSpot JVM
heap memory

Java 8 HotSpot JVM
heap memory

Young Generation

新しいオブジェクトが最初に生成されたとき、heap memory 内で配置される空間である。

Young Generation は次のように分けられる。

  • Eden
    • Java オブジェクトが生成されるとすぐに割り当てられるメモリ領域である。つまり、new 演算子で新しく生成されたオブジェクトが配置される。
  • Survivor1, Survivor2
    • 各領域が埋まると、生き残ったオブジェクトは空いている Survivor へ移動する。
    • このとき参照がないオブジェクトは Minor GC によって収集される。

Old Generation

Young Generation でガベージコレクションを最後まで生き残ったオブジェクトが Old Generation へ移動する。つまり、古いデータが保存される領域である。
Major GC が実行され、Minor GC より回数は少ない。

Permanent Generation

Permanent Generation は Class または Method Code が保存される領域である。PermGen は Heap 領域に属する。
PermGen は Permanent Generation の略で、JVM がロードしたクラスのメタデータを追跡するために使用する、通常の Java ヒープとは別の特殊なヒープ空間である。
Permanent Generation には、ロードされたクラスの情報など、ある程度変化しないことが保証されるデータが保存される。

デフォルトでは制限されたサイズを持つ。(32-bit JVM: 64MB、64-bit JVM: 82MB)

JVM Argument

-XX:PermSize=N      --> PermGen Default Size を設定。
-XX:MaxPermSize=N   --> PermGen Max Size を設定。

Metaspace Generation

Java 8 が登場して JVM 領域に変化があった。JVM の複数のメモリ領域のうち Permanent Generation メモリ領域がなくなり、Metaspace 領域が作られた。 Metaspace は Java 8 以前の Perm 領域を置き換えるもので、クラスとメソッドのメタデータが保存される領域である。

  • Metaspace は、Java の Classloader がこれまでロードした Class の Metadata が保存される空間である。
  • 重要なのは、Heap 領域ではなく Native メモリ領域に位置する点である。
  • デフォルトでは制限されたサイズを持たない。そのため、必要なだけ増え続ける。
  • Java 8 以降は PermGen 関連の JVM オプションは無視される。

JVM Argument

-XX:MetaspaceSize=N
-XX:MaxMetaspaceSize=N
  • MetaspaceSize: Metaspace Default Size を設定。
    • この設定は JVM が使用するネイティブメモリ量を変更するために使われる。
    • システムが基本で提供するものより多くのメモリを使用すると確信できる場合に、このオプションを使うとよい。
  • MaxMetaspaceSize: Metaspace Max Size を設定。
    • この設定は metaspace の最大メモリ量を変更するために使われる。
    • アプリケーションをサーバーで動作させるときにメモリ領域を調整したい場合や、メモリリークが発生してシステム全体のネイティブメモリを使い切らないようにしたい場合に使う。
    • もし native メモリがいっぱいになっているにもかかわらず、アプリケーションがさらにメモリを要求すると java.lang.OutOfMemoryError: Metadata space が発生する。
    • 設定を指定しなければ制限は設けられない。

Permanent と Metaspace

Java 8 で PermGen は、いくつかの違いはあるものの Metaspace へ名前が変わった。ここで重要なのは、Metaspace にはデフォルトの最大サイズが無制限である点だ。反対に Java 7 以下の PermGen は、デフォルト最大サイズが 32 ビット JVM で 64MB、64 ビット版で 82MB である。もちろん初期サイズと同じではない。Java 7 およびそれ以前のバージョンは、初期 PermGen 空間の約 12-21MB で開始する。

Permanent と Metaspace のデフォルト制限サイズを比較すると次のようになる。

JVM Default maximum PermGen size (MB) Default maximum Metaspace size
32-bit client JVM 64 unlimited
32-bit server JVM 64 unlimited
64-bit JVM 82 unlimited

Java 7 以前には、インターンされた文字列(Interned Strings)が PermGen に保持されていた。そのため、悪名高い次のような深刻なエラーが発生していた。

java.lang.OutOfMemoryError: PermGen space

PermGen/Metaspace のサイズを調整する必要があるたびに、JVM は標準ヒープと同じようにサイズを調整する。この空間のサイズ調整には Full GC が必要であり、これは常にコストの高い処理である。多くのクラスがロードされる起動中によく観察できる。特にアプリケーションが多くの外部ライブラリに依存している場合はなおさらである。起動中に Full GC が多く発生する場合、通常はこれが原因である。このような場合、初期サイズを増やすと起動性能が向上することがある。

PermGen から Metaspace への変更の長所と短所は次のとおりである。

長所

  • PermGen 領域が削除され、heap 領域で使用できるメモリが増えた。
  • PermGen 領域を削除するために存在していた複雑なコードが削除され、PermGen 領域をスキャンするために消費されていた時間が減少し、GC 性能が向上した。

短所

  • もはや PermGen 空間が不足することはないが(PermGen が存在しないため)、過度なネイティブメモリを消費してプロセス全体のサイズが大きくなる可能性がある。
  • 問題は、アプリケーションが多くのクラスや文字列をロードする場合、実際にアプリケーションだけでなくサーバー全体を停止させる可能性があることだ。これはネイティブメモリがオペレーティングシステムによってのみ制限されるためである。つまり、そのままサーバーのすべてのメモリを占有し得る。

Java コードによるメモリ管理

Java コードを分析し、ここでメモリをどのように管理するかを見てみよう。

class Person {
    int id;
    String name;

    public Person(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

public class PersonBuilder {
    private static Person buildPerson(int id, String name) {
        return new Person(id, name);
    }

    public static void main(String[] args) {
        int id = 23;
        String name = "John";
        Person person = null;
        person = buildPerson(id, name);
    }
}
  1. main() メソッドに入ると、スタックメモリにこのメソッドのプリミティブ要素と参照を保存するための空間が作成される。
    • スタックメモリは整数 id のプリミティブ値を直接保存する。
    • Person 型の参照変数 person もスタックメモリに作成され、ヒープ内の実際のオブジェクトを指す。
  2. main() からパラメータ付きコンストラクタ Person(int, String) を呼び出すと、以前のスタックの上に追加メモリが割り当てられる。これには次のものが保存される。
    • スタックメモリにある呼び出しオブジェクトの this オブジェクト参照
    • スタックメモリのプリミティブ値 id
    • ヒープメモリの文字列プールにある実際の文字列を指す、文字列引数 name の参照変数
  3. main メソッドはさらに buildPerson() static メソッドを呼び出しており、このメソッドに対して以前のメソッドの上にスタックメモリで追加割り当てが発生する。これにより、上で説明した方式で変数が再び保存される。
  4. ただし、ヒープメモリは Person 型の新しく生成されたオブジェクト person に対するすべてのインスタンス変数を保存する。

この割り当てについては、次の図で確認する。

Java heap stack diagram 出典: https://www.baeldung.com/java-stack-heap