本ページはプロモーションが含まれています

Java入門

図で動かすJVMメモリ|スタックとヒープ・参照・GCの関係

CATEGORY
REV.
DRAWN BY
TOM
SHEETS
6枚
Stack / Heap(図で動かすJVMメモリ)

トム

SESで約8年、環境を変えられないもどかしさの中にいました。32歳で自社開発企業に転職し、今はClaude Codeと自動化で、仕事も時間も組み直している途中です。
・Javaバックエンドエンジニア(Java歴12年以上)
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎
詳細なプロフィール

𝕏

「変数には値が入っている」——最初に習うこの言い方が、あとで必ず足を引っかけてきます。

== で比較したら false になる。メソッドに渡した変数が書き換わっている。あるいは、書き換わっていない。nullにしたはずのオブジェクトがメモリから消えない。

これらは全部、スタックとヒープで何が起きているかを1枚の図にすれば同時に解けます。

下のページでは、コードを1行ずつ実行しながら、その図が動きます。矢印が「参照」、灰色になった箱が「どこからも辿れなくなったもの」です。GCの判定は説明用の演出ではなく、スタックとstaticを出発点に参照を実際にたどって計算しています。

INSIDE THE JVM MEMORY

変数は、箱そのものではない

コードを1行ずつ実行して、スタックとヒープに何が起きるかを見ます。矢印が「参照」、灰色になった箱が「もう誰も辿れないもの」です。

コード

    メモリ

    スタックメソッドごとの作業台

    ヒープnewで作られた箱の置き場

    ヒープの中は3つの区画に分かれています。新しい箱はまずEdenに置かれ、GCを生き延びるたびに右の区画へ引っ越します。

    Edenエデン=新しい箱が最初に置かれる場所

    SurvivorGCを1回生き延びた箱の引っ越し先

    Old何度も生き延びた、長生きの箱の置き場

    到達できるかどうかは、スタックとstaticを出発点にして、矢印を実際にたどって判定しています(マーク&スイープ)。

    スタックとヒープの役割分担

    スタックは、メソッドごとの作業台です。メソッドを呼ぶと1枚積まれ、終わると捨てられます。ここに置かれるのは、intのような基本型の値そのものと、オブジェクトの置き場所(参照)だけです。

    ヒープは、newで作られた箱の置き場です。こちらはメソッドが終わっても消えません。誰からも辿れなくなったとき、GCが回収します。

    この線引きが、次の3つの「なぜ」をまとめて説明します。

    「変数は箱ではない」がすべての出発点

    String b = a; でコピーされるのは、箱の中身ではなく置き場所です。上のページの1つめのタブで、矢印が2本になって同じ箱を指すのが見えます。

    だから == は「矢印の先が同じか」を、equals は「箱の中身が同じか」を比べています。同じ文字列でもnewすれば別の箱になるので、== は false。この1枚を覚えておけば、文字列比較で悩むことはなくなります。

    メソッド呼び出しも同じ理屈です。Javaは引数を必ずコピーして渡します。intなら値のコピーなので呼び出し元は無傷、オブジェクトなら場所のコピーなので、たどった先の箱を書き換えれば呼び出し元からも見えます。「参照渡し」と呼ばれがちですが、正確には参照を値渡ししている、というのが2つめのタブで見えるものです。

    GCが見ているのは「たどれるか」だけ

    3つめのタブがGCの本質です。d2 = null; としても、d1.next から辿れるうちは回収されません。逆に、変数が1つも指していなくても、どこか1か所から辿れれば生き続けます。

    GCが数えているのは参照カウントではなく、到達可能性です。スタックとstaticを出発点(GCルート)にして矢印をたどり、たどり着けなかったものだけを回収する。これがマーク&スイープで、上のページでも実際にその計算をしています。

    だから、互いに参照し合っている2つのオブジェクトも、外から辿れなくなれば両方まとめて回収されます。循環参照でメモリが残らないのはこのためです。

    メモリリークは「消えないゴミ」ではなく「捨て忘れ」

    4つめのタブは、staticなリストに入れ続けるだけのコードです。GCを何度押しても、ひとつも減りません。staticはプログラムが動いているあいだ、ずっとGCルートであり続けるからです。

    Javaのメモリリークは、「GCが壊れている」のではなく「まだ辿れてしまう」状態です。キャッシュ、リスナー登録、ThreadLocal、staticなMap。どれも入れたものを取り出し忘れると、同じ図になります。

    対策も図から素直に出てきます。使い終わったらremoveする、参照を持つ期間を短くする、あるいはWeakHashMapのように「他から辿られなくなったら消えていい」参照を使う。どれも、矢印を減らす話です。

    世代別GCとStop the World

    5つめのタブでは、ヒープの中が3つの区画に分かれます。名前だけ見ても意味が分からないので、先に整理しておきます。

    • Eden(エデン):newした箱が最初に置かれる場所。エデンの園=生まれる場所、という名前です。
    • Survivor(サバイバー):GCを1回生き延びた箱の引っ越し先。生存者、という意味そのままです。
    • Old(オールド):何度も生き延びた、長生きの箱が最後に落ち着く場所。老年世代とも呼ばれます。

    なぜわざわざ分けるのか。「ほとんどのオブジェクトは生まれてすぐ死ぬ」という経験則があるからです。ループの中で作った一時オブジェクトは、次の周回には用済みになります。生き残るのはごく一部です。

    新しい箱はまずEdenに置かれ、Edenが埋まるとMinor GCが走ります。生き残った少数だけをSurvivorへ移し、生き残り続けたものをOldへ昇格させる。死ぬ前提の場所だけを頻繁に掃除するので、毎回ヒープ全体を見に行かずに済みます。

    そしてGCの最中は、アプリケーションのスレッドが止まります。上のページで赤いバーが伸びるのがそれで、これがStop the Worldです。Eden中心に新世代だけを見るMinor GCは短く、Oldまで含むFull GCは長い。「たまにアプリが数秒固まる」の正体は、たいていこちらです。

    あわせて読む

    図が読めると、対処の順番が変わる

    メモリが増え続けるとき、最初にやるべきはヒープサイズを増やすことではありません。どこからの矢印が切れていないのかを探すことです。ヒープを増やしても、捨て忘れが直らなければ到達するまでの時間が延びるだけです。

    逆に、一時的に大量のオブジェクトを作る処理でGCが頻発しているなら、それはEdenの広さやアルゴリズムの選択の話になります。同じ「メモリが苦しい」でも、見る場所がまったく違うわけです。

    この図が頭に入っていれば、==の罠も、引数の書き換わりも、リークも、GCログの読み方も、全部ひとつの絵の上に載ります。まずは上のページで、矢印が切れる瞬間だけでも見ていってください。

    REVISIONS / 改訂履歴
    REV.DATE内容
    22026-10-06記載内容の事実確認と修正
    12026-09-23初版
    NEXT READ本棚から1本

    AIで開発を組み直すなら、まずこの5冊から

    入門・実践・設計まで、Claude Codeの本をレベル別に5冊選んで比べました。

    → おすすめ5冊を見る

    • この記事を書いた人
    • 最新記事

    トム

    SESで約8年、環境を変えられないもどかしさの中にいました。32歳で自社開発企業に転職し、今はClaude Codeと自動化で、仕事も時間も組み直している途中です。
    ・Javaバックエンドエンジニア(Java歴12年以上)
    ・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎
    詳細なプロフィール

    𝕏

    Java入門