JVM メモリ構造をざっくり一枚の絵にする
まずイメージからいきます。
JVM のメモリは、大きく言うと次のような「部屋」に分かれています。
- ヒープ(Heap):new したオブジェクトが住む場所
- スタック(Stack):メソッド呼び出しの一時的な作業台
- メソッド領域/メタスペース:クラスやメソッドの情報が置かれる場所
- そのほか、スレッドごとの小さな領域やネイティブコード用の領域
この「部屋の役割」が分かると、
「なぜ GC が必要なのか」「なぜスタックオーバーフローが起きるのか」
といった話が一気につながります。
ヒープ(Heap):new したものが全部集まる「オブジェクトの街」
何が置かれる場所か
ヒープは、new したオブジェクトや配列が置かれる場所です。new String("hello") も、new int[10] も、new ArrayList<>() も、全部ヒープに置かれます。
Java の「参照型」は、基本的にここに住みます。
変数が持っているのは「ヒープ上のオブジェクトへの参照(住所)」です。
String s = new String("hello");
Javaこのとき、s という変数はスタックに置かれ、
実体の "hello" オブジェクトはヒープに置かれます。
ヒープはなぜ GC とセットなのか
ヒープに置かれたオブジェクトは、
「もうどこからも参照されなくなったら、GC が回収してくれる」
というルールで管理されています。
だから Java では、C のように free や delete を書かなくていい。
代わりに、ヒープは「GC が掃除する前提でどんどん散らかる場所」だと思ってください。
ヒープの中の「世代」:Young / Old
実際の JVM では、ヒープはさらに「世代」に分かれます。
- Young(若い世代):生まれたばかりのオブジェクト
- Old(年老いた世代):長生きしているオブジェクト
多くのオブジェクトは「すぐ死ぬ(すぐ使われなくなる)」という経験則があるので、
Young 領域は「頻繁に、素早く GC する」ように設計されています。
Young で何回か GC を生き延びたオブジェクトは、Old に昇格します。
Old は「長生き組」なので、GC の頻度は低いけれど、一回の GC は重めです。
この「世代別 GC」の仕組みが、Java のパフォーマンスチューニングの土台になります。
スタック(Stack):メソッド呼び出しの「作業台」
スレッドごとに一つずつある
スタックは「スレッドごと」に用意されます。
スレッドが一つ増えると、そのスレッド専用のスタックが一つ増えます。
スタックには、メソッド呼び出しごとに「スタックフレーム」と呼ばれる枠が積み上がります。
void a() {
int x = 10;
b(x);
}
void b(int y) {
int z = y + 1;
}
Javaa() を呼ぶとき、a 用のフレームがスタックに積まれ、
その中にローカル変数 x や引数などが置かれます。
a の中から b を呼ぶと、b 用のフレームがその上に積まれ、y や z がそこに置かれます。
メソッドが終わると、そのフレームはスタックからポンと外されます。
だから、スタック上のデータは「メソッドの寿命と一緒」です。
スタックに置かれるもの・置かれないもの
スタックに置かれるのは、主に次のようなものです。
- プリミティブ型のローカル変数(
int,long,doubleなど) - 参照型の「参照(ポインタ)」
- 戻り先アドレスや一時的な計算用の値
new したオブジェクトの「中身」はスタックには置かれません。
スタックにあるのは「そのオブジェクトへの参照」だけです。
スタックオーバーフローはなぜ起きるのか
スタックはサイズに上限があります。
再帰呼び出しを深くしすぎると、フレームが積み上がりすぎて、
スタックの上限を超えます。
void recurse() {
recurse();
}
Javaこれを呼ぶと、延々とフレームが積まれていき、StackOverflowError が発生します。
「スタックはメソッド呼び出しのための有限の積み重ねスペース」
というイメージを持っておくと、このエラーの意味が腑に落ちます。
メソッド領域/メタスペース:クラスの「設計図置き場」
何が置かれる場所か
JVM は、クラスをロードするときに、
そのクラスの「メタ情報(設計図)」を専用の領域に置きます。
そこには、例えばこんな情報が入っています。
- クラス名、パッケージ名
- フィールドやメソッドの一覧
- メソッドのバイトコード
- 定数プール(リテラル文字列など)
昔の JVM ではこれを「メソッド領域(Method Area)」や「PermGen」と呼んでいましたが、
今の HotSpot JVM では「メタスペース(Metaspace)」という形でネイティブメモリ側に置かれます。
なぜここを意識する必要があるのか
普通のアプリではあまり意識しませんが、
大量のクラスを動的に生成・ロードするようなフレームワーク(JPA、Proxy、動的クラス生成など)を使うと、
この領域がパンパンになって OutOfMemoryError を起こすことがあります。
「オブジェクトが多すぎてヒープが足りない」のとは別に、
「クラスの設計図が多すぎてメタスペースが足りない」というパターンもある、
ということだけ頭の片隅に置いておくと、エラーの意味が読みやすくなります。
簡単なコードで「どこに何が置かれるか」を追ってみる
例コード
public class MemoryExample {
public static void main(String[] args) {
int a = 10;
String s = new String("hello");
int[] arr = new int[3];
arr[0] = a;
print(s, arr);
}
static void print(String msg, int[] values) {
int len = values.length;
System.out.println(msg + " : " + len);
}
}
Javaこれをメモリ構造の観点で眺めてみます。
main が動き始めたとき
JVM 起動時に、MemoryExample クラスの情報がメタスペースにロードされます。main が呼ばれると、main 用のスタックフレームが積まれます。
そのフレームの中に、ローカル変数として a, s, arr が置かれます。
a:スタック上のint値(10)s:ヒープ上のStringオブジェクトへの参照arr:ヒープ上のint[]への参照
new String("hello") と new int[3] の実体はヒープに置かれます。
print を呼んだとき
print(s, arr) を呼ぶと、print 用のスタックフレームがその上に積まれます。
そのフレームには、引数 msg, values、ローカル変数 len が置かれます。
msg:sと同じStringへの参照values:arrと同じint[]への参照len:values.lengthの結果(3)
ここでも、ヒープ上のオブジェクトは増えていません。
増えたのは「参照」と「プリミティブ値」だけです。
print が終わると、そのフレームはスタックから消えます。main が終わると、main のフレームも消えます。
その後、s や arr を指していた参照も消えるので、
ヒープ上の String と int[] は「どこからも参照されない」状態になります。
次の GC で回収対象になります。
パフォーマンスの観点から見た JVM メモリ構造のポイント
「短命オブジェクトを大量に作る」と何が起きるか
短命なオブジェクト(すぐに使い捨てるオブジェクト)を大量に作ると、
Young 領域での GC が頻繁に起きます。
例えば、巨大なループの中で毎回 new StringBuilder() しているようなコードは、
GC の負荷を上げる典型例です。
ただし、JVM は短命オブジェクトの回収はかなり得意なので、
「短命オブジェクトが多い=即アウト」ではありません。
大事なのは、
- 不必要なオブジェクトを作りすぎていないか
- 長生きする大きなオブジェクトを無駄に残していないか
を意識することです。
「参照を持ち続ける」と GC できない
ヒープのオブジェクトは「どこからも参照されなくなったとき」に初めて GC 対象になります。
逆に言うと、
「もう使わないのに、どこかのコレクションに入れっぱなし」
「static フィールドでずっと参照を持ち続けている」
といった状態だと、GC はそのオブジェクトを回収できません。
これがいわゆる「メモリリーク」の正体です。
C のように「解放し忘れ」ではなく、
「参照を持ち続けてしまった結果、GC が回収できない」という形で起きます。
スタックは「軽いが有限」、ヒープは「重いが柔軟」
スタック上の変数は、メソッドが終われば自動的に消えます。
GC の対象にもなりません。
その意味で「軽い」領域です。
ヒープは、GC の対象になる分、管理コストがあります。
でも、サイズは大きく、柔軟にオブジェクトを置けます。
「何でもかんでもヒープに置く」のではなく、
「一時的な計算はローカル変数(スタック)で済ませる」
という意識を持つと、自然と無駄なオブジェクトが減っていきます。
まとめ:JVM メモリ構造を自分の言葉で説明するなら
あなたの言葉でまとめると、こうなります。
「JVM のメモリは、new したオブジェクトが住むヒープ、
メソッド呼び出しごとの一時的な作業台であるスタック、
クラスの設計図を置くメタスペース(メソッド領域)などに分かれている。
スタックはスレッドごとにあり、メソッドが終わればフレームごと消える。
ヒープは GC によって『もう参照されていないオブジェクト』が回収される。
ヒープは Young / Old の世代に分かれ、短命オブジェクトと長命オブジェクトを分けて効率よく GC している。
パフォーマンスやメモリトラブルを考えるときは、
『この値はスタックかヒープか』『このオブジェクトはどれくらい生きるか』
『どこが参照を持ち続けているか』を意識してコードを見ることが大事になる。」

