1. Javaプロセスのメモリ概観
Javaプログラムを起動すると、JVM (Java Virtual Machine) はOSにメモリ領域を要求します。少ないこともあれば、多いこともあります(とくに多数のMOD付きのMinecraftのような場合)。このメモリは複数の主要な領域に分かれ、それぞれに役割があります:
- スタック (Stack) — ローカル変数とメソッド呼び出し用。
- ヒープ (Heap) — newで作成するすべてのオブジェクト用。
- 管理領域 (PermGen/MetaSpace) — クラスのメタデータ、staticフィールド、その他の「魔法のような」もの。
概念図は次のとおり:
┌───────────────────────────────┐
│ JVMプロセス │
│ ┌─────────────┐ │
│ │ Stack │ ← 各スレッドごとに専用のスタック
│ └─────────────┘ │
│ ┌─────────────┐ │
│ │ Heap │ ← 全スレッドで共有
│ └─────────────┘ │
│ ┌───────────────┐ │
│ │ PermGen/ │ ← クラスのメタデータ
│ │ MetaSpace │
│ └───────────────┘ │
└───────────────────────────────┘
なぜ重要か?
- メモリ構造を理解すると、より効率的で安全なコードが書けます。
- StackOverflowErrorやOutOfMemoryErrorといったエラーの診断が容易になります。
- 「ガーベジコレクタ」や「メモリリーク」といった言葉も怖くありません—どこをどう調べればよいか理解できます。
2. スタック (Stack): 高速・局所・だが永続ではない
スタックは各スレッドごとに個別に確保される特別なメモリ領域です。皿の山のようなもので、最後に置いたものから先に取り出します。つまりLIFO (Last In, First Out) の原理で動きます。
スタックは何のため?
スタックに格納されるもの:
- メソッドのローカル変数(たとえばメソッド内のint x = 5;)。
- メソッド呼び出し後の復帰アドレス(メソッド終了後にどこへ戻るか)。
メソッドを呼び出すたびに、新しいフレーム(stack frame)がスタックに追加されます。これは、そのメソッドのローカル変数や管理情報が入った箱のようなものです。メソッドが終了すると、そのフレームは取り除かれ、ローカル変数は消えます。
例
public static void main(String[] args) {
int a = 10; // aはmainのスタックにある
int b = sum(a, 5); // sumを呼び出す
}
public static int sum(int x, int y) {
int result = x + y; // x, y, resultはsumのスタックにある
return result;
}
- sumが呼び出されると、そのための別のフレームがスタックに作られます。
- sumの処理が終わると、その変数は消えます。
変数のライフサイクル
ローカル変数は、宣言されたメソッドが実行されている間だけ生きています。メソッドが終了した瞬間に存在しなくなり、メモリは即座に解放されます。
スタックオーバーフロー
誤って(あるいは意図的に)無限再帰を書いてしまうと、呼び出しのたびに新しいフレームがスタックに追加されます。やがてスタックが尽き、次のようになります:
Exception in thread "main" java.lang.StackOverflowError
例:
public static void main(String[] args) {
recurse();
}
public static void recurse() {
recurse(); // 無限再帰!
}
スタックサイズ
スタックのサイズには上限があります—通常はスレッドごとに数MB(パラメータ-Xssで指定可能)。スタックが尽きるとプログラムはエラーで終了します。
3. ヒープ (Heap): オブジェクトの居場所
ヒープ(Heap)はすべてのスレッドで共有される領域で、newで作成したすべてのオブジェクトや配列が存在します。オブジェクト指向の「魔法」は、このヒープで起こります。
オブジェクトはどうやってヒープに置かれる?
String s = new String("Hello");
int[] arr = new int[10];
- 変数sは参照で、スタックに置かれます。
- Stringの実体と配列arrはヒープに置かれます。
オブジェクトのライフサイクル
オブジェクトは、少なくとも1つの強参照(strong reference)が存在する限りヒープに生きています。誰からも参照されなくなった瞬間に「ごみ」となり、ガーベジコレクタ(GC)によって削除可能になります。
メモリ管理
C/C++とは異なり(freeやdeleteで開放を自分で行う)、JavaではGCが担当します。オブジェクトを明示的に解放することはできませんが、すべての参照を切れば削除の候補になります。
図: どこに何がある?
Stack (main)
└─ s ─┬────────────┐
│ │
▼ │
Heap │
┌─────────────┐ │
│ String "Hello"◄──┘
└─────────────┘
ヒープの特徴
- ヒープはJVMプロセス内で1つだけです。
- ヒープサイズは起動時に指定できます(-Xmx、-Xms)。
- ヒープの空きがなく、GCでも回収できない場合、プログラムはOutOfMemoryErrorで落ちます。
4. PermGen と MetaSpace: クラスはどこに存在する?
class MyClass { ... }と書いてプログラムを実行すると、JVMはそのクラスに関するすべて—メソッド、フィールド、バイトコード、static変数、定数、さらには文字列リテラルまで—をどこかに保持する必要があります。そのためにJVMには、クラスが「住む」特別なメモリ領域があります。
かつて、Java 8以前はこの領域をPermGen(Permanent Generation)と呼んでいました。しかし問題も少なくなく、例えばサイズが固定で、領域が足りなくなるとOutOfMemoryError: PermGen spaceでアプリケーションが落ちていました。
Java 8以降は、より柔軟なMetaSpaceが導入され、古いPermGenを置き換えました。MetaSpaceは必要に応じて自動的に拡張でき(物理メモリの範囲内)、より扱いやすくなりました。
PermGen (Java 8 以前)
- PermGenにはクラスのメタデータ、staticフィールド、文字列リテラルが格納されていました。
- PermGenのサイズは制限があり(デフォルトは小さめ)、-XX:MaxPermSize=256mで増やせました。
- 動的に大量のクラスを読み込むアプリケーション(例: Webサーバー)では、PermGenが「尽きて」次のエラーが発生しました:
java.lang.OutOfMemoryError: PermGen space
- 問題点: クラスが動的にアンロードされる場合(例: Webアプリの再起動時)、PermGenのクリーンアップが常に正しく行われるとは限りませんでした。
MetaSpace (Java 8+)
- Java 8でPermGenが廃止され、MetaSpaceが導入されました。
- MetaSpaceはクラスのメタデータを保持しますが、今度はネイティブメモリ(Javaヒープの外)に置かれます。
- MetaSpaceのサイズはデフォルトでは無制限(システムメモリにより制限)ですが、-XX:MaxMetaspaceSize=512mで上限を設定できます。
- メモリ不足時のエラーは次のようになります:
java.lang.OutOfMemoryError: Metaspace
- MetaSpaceにはstaticフィールド、メソッド、クラス情報も格納されます。
図: 構成イメージ
┌───────────────────────────────┐
│ JVMプロセス │
│ ┌─────────────┐ │
│ │ Stack │ ← ローカル変数、メソッド呼び出し
│ └─────────────┘ │
│ ┌─────────────┐ │
│ │ Heap │ ← オブジェクト、配列、newで作られるもの
│ └─────────────┘ │
│ ┌───────────────┐ │
│ │ MetaSpace │ ← クラスのメタデータ、staticフィールド
│ └───────────────┘ │
└───────────────────────────────┘
なぜ重要か?
通常のデスクトップやサーバーアプリでは、PermGenやMetaSpaceのエラーに遭遇しないかもしれません。しかし、動的なクラスロードを扱う(例: プラグイン、Webアプリ、Springのように大量のクラスをロード/アンロードするフレームワーク)場合、MetaSpaceに関する知識は必須!
5. 図解: JVMメモリ構成
flowchart TD
subgraph JVM
direction TB
Stack1["Stack (Thread 1)"]
Stack2["Stack (Thread 2)"]
Heap[Heap]
MetaSpace[MetaSpace]
end
Stack1 --を参照--> Heap
Stack2 --を参照--> Heap
Heap --のクラスを使用--> MetaSpace
- 各スレッドには専用のスタックがあります。
- すべてのスタックはヒープ上のオブジェクトを参照できます。
- ヒープ上のオブジェクトは自分のクラスを「知っており」、その情報はMetaSpaceにあります。
6. 例: 実際のコードでどう見えるか
public class MemoryDemo {
public static void main(String[] args) {
int x = 42; // x は main のスタックにある
String s = "Hello!"; // s — 参照はスタック、String の実体はヒープ、リテラル "Hello!" は MetaSpace
Person p = new Person("Alice"); // p — 参照はスタック、Person の実体はヒープ
// 新しいスタックフレームを作るためにメソッドを呼び出す
printPerson(p);
}
public static void printPerson(Person person) {
// person — printPerson のスタック上の参照
System.out.println(person.getName());
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() { return name; }
}
解説:
- x — ローカル変数で、mainのスタックに存在します。
- s — 参照はスタック、Stringの実体はヒープ、文字列リテラル"Hello!"はMetaSpace。
- p — 参照はスタック、Personの実体はヒープ。
- Personクラスとそのメソッド/フィールドはMetaSpace(クラスのメタデータ)。
- printPerson(p)の呼び出しで新しいスタックフレームが作られ、その中のローカル参照personは同じヒープ上のオブジェクトを指します。
7. JVMのメモリ管理: 簡易FAQ
スタックを自分で管理できますか?
いいえ、スタックはJVMが完全に管理します。起動時にサイズ(-Xss)を指定できるだけです。
ヒープを自分で管理できますか?
一部できます: ヒープサイズは起動時に(-Xmx、-Xms)指定できます。回収はガーベジコレクタ(GC)が担当します。
MetaSpace を管理できますか?
サイズ上限(-XX:MaxMetaspaceSize)を設定できますが、通常は不要です。
メモリ不足のときはどうなりますか?
— スタックが尽きた場合 — StackOverflowError。
— ヒープが尽きた場合 — OutOfMemoryError: Java heap space。
— MetaSpace が尽きた場合 — OutOfMemoryError: Metaspace。
8. メモリ関連でよくあるミス
エラー1: 無限再帰による StackOverflowError。 最もよくある原因は、再帰の終了条件を用意し忘れることです。たとえばメソッドが止まらずに自分自身を呼び続けると、JVMはスタックを無限に拡張できず、プログラムは「落ち」ます。
エラー2: ヒープ枯渇による OutOfMemoryError。 非常に多くのオブジェクトを作成し、それらへの参照(変数/コレクション)が残り続けると(例: リストに追加し続けて一度も削除しない)、ヒープが尽きます。
エラー3: OutOfMemoryError: PermGen space / Metaspace。 プラグインや動的クラスロードを使い、かつMetaSpaceが解放されない(例: クラスのアンロードが正しくない)場合、MetaSpaceの空きがなくなることがあります。
エラー4: 参照とオブジェクトの取り違え。 多くの初心者が混同します: Person型の変数はスタック上の参照にすぎず、実体のオブジェクトはヒープにあります。
エラー5: GCが即座にすべてを回収すると期待する。 GCは「気分」で動く(実際には内部アルゴリズムやメモリ逼迫に応じて動作)ので、参照を消した直後に必ず解放されるとは限りません。即時解放を当てにしないでください。
GO TO FULL VERSION