PR

Javaのメモリ管理の仕組みを1からわかりやすく

Java

Javaは「一度書けばどこでも動く (Write Once, Run Anywhere)」というコンセプトで誕生し、オブジェクト指向プログラミングと自動メモリ管理(ガベージコレクション)を特徴としています。C/C++と比べてメモリ管理の負担が少ないため、多くのエンジニアが重宝している言語ですが、“どのようにメモリが管理されているのか” を深く理解している人は意外に多くありません。

しかし、Javaのメモリ管理の仕組みを理解しておくと、以下のような利点があります。

  • アプリケーションがメモリ不足に陥ったときの原因を素早く特定できる
  • ガベージコレクションのチューニング方法を理解し、パフォーマンス改善に活かせる
  • クラスローディングの仕組みを把握し、複雑なアプリケーション構成でもクラスパス問題や依存関係のトラブルに対処できる

このページでは、Javaが提供する「スタック」「ヒープ」「メソッド領域(メタスペース)」などのメモリ領域と、その背後で動作するクラスローディングやガベージコレクションなどの仕組みを、なるべく噛み砕いて解説します。

スポンサーリンク

Javaのメモリ領域の全体像

Javaアプリケーションが動作している最中、JVMで使用するメモリは大きく以下の領域に分けて使われています。

  1. スタック (Stack)
  2. ヒープ (Heap)
  3. メソッド領域 (Method Area) / メタスペース (Metaspace)
  4. (広義の)Static領域

それぞれ役割や管理方法が異なり、理解することでJavaプログラムの動きを俯瞰できるようになります。

OSメモリとJVMメモリの違いを“オフィスビル”に例える
  • OSメモリ = “オフィスビル全体の空間”
    たとえば、ビルの各フロアはさまざまな企業や部署が借りる可能性があるし、共有スペースも存在します。コンピュータ全体では、OS(オペレーティングシステム)がこの「ビルオーナー」のような立場です。ビル全体のスペースを区画に分け、各アプリケーション(プロセス)に「ここを使っていいよ」という形で割り当てています。
  • JVMメモリ = “ビル内でJavaが借りているフロア”
    Javaプログラムを動かすためのJVMは、ビル(OSメモリ)の一部フロアを借りてオフィスを構えます。もちろん、そのフロアをどれだけ借りられるか(どれだけメモリを使用できるか)は、OSの許可や設定(-Xmsや-Xmxなど)によって決まります。つまり、「ビル全体の中の、Java専用のオフィス空間」がJVMメモリというイメージです。

スタック (Stack)

用途:

  • メソッド呼び出しごとのローカル変数や演算途中の一時値を格納する場所
  • メソッド開始時に「スタックフレーム」を積み、終了時に破棄される (LIFO - Last In First Out)

特徴:

  • 高速アクセス: push/popによる領域の確保・解放が簡単。
  • メモリサイズに制限: -Xss オプションなどでスレッドごとに割り当てるスタックサイズを設定可能。再帰が深すぎるとStackOverflowErrorを起こすことがある。
  • スレッド単位で分離: 各スレッドは自前のスタックを持つため、あるスレッドのスタックオーバーフローが他スレッドに直接影響するわけではない。

スタックはメソッドの引数やローカル変数だけでなく、バイトコードの実行時に使う演算用の一時領域(オペランドスタック)も含まれます。メソッドが終了するたびにフレームが破棄される仕組みは、「スコープを抜ければ変数が消える」というローカル変数の振る舞いそのものです。

ヒープ (Heap)

用途:

  • new キーワードで生成されるオブジェクト本体や配列を配置する領域
  • ほぼすべてのオブジェクトはヒープに置かれ、ガベージコレクションの対象となる

特徴:

  • ガベージコレクション(GC) によって不要オブジェクトが自動回収される
  • -Xms-Xmx オプションなどで初期サイズや最大サイズを設定できる
  • 世代別に分割される(Young世代・Old世代 など)ことで効率的にGCを行う仕組みがある

Javaのオブジェクトはすべてヒープに置かれ、その参照をスタック上などで持つのが基本パターンです。もしスタックから参照が切れてしまうと、いずれGCによって回収され、プログラマは手動でfree()deleteを呼び出す必要がありません。この仕組みがJavaのメモリ管理を大幅に楽にしています。

ヒープとスタックの違いを“倉庫とデスク”に例える

Javaが使うメモリ空間(JVMメモリ)の中でも、「ヒープ (Heap)」と「スタック (Stack)」は、利用シーンや管理方式がまったく異なります。オフィスフロアに置き換えて考えてみましょう。

ヒープ = “フロアの倉庫や共有ロッカー”

  • ヒープは大きな倉庫や共有ロッカー
    たとえば会社に大きな物品置き場や共有倉庫があって、何か必要なものを取り出すときはみんなそこに入れたり出したりします。Javaで言えば、new キーワードを使って必要な「モノ(オブジェクト)」をヒープに置くイメージです。
  • 誰が持っているかはタグ(参照)で管理
    倉庫にはいろいろな道具や書類(=オブジェクト)が置かれます。それらがどの人(メソッド)に属しているかは「タグ」や「伝票」(オブジェクト参照)で管理します。倉庫にある物自体は常にそこに置かれ、タグを外される(=誰からも参照されなくなる)と“いずれ撤去される”というわけです(※ガベージコレクションの話は省略しますが、イメージとして残しておくと理解しやすいでしょう)。

スタック = “デスクの引き出し”

  • スタックは各社員(スレッド)の個人デスクの引き出し
    オフィスでは、一人ひとりがデスクを持っていて、その引き出しに書類(メソッドのローカル変数)をしまいます。メソッド呼び出しのたびに「引き出し」が1段増え、メソッド終了で「引き出し」を閉じるイメージです。
  • 使い終わればすぐ片づく
    メソッドが終了すれば、その引き出し(スタックフレーム)は一気に片付いて“なかったこと”になります。大量の書類を引き出しに入れすぎると(ローカル変数を巨大にしすぎると)、引き出しがパンクして(スタックオーバーフロー)しまうので注意が必要、というように考えるとわかりやすいでしょう。

メソッド領域 (Method Area) / メタスペース (Metaspace)

用途:

  • JVMがロードしたクラス情報やメソッド定義、定数プール、JITコンパイルされたコードなどを格納
  • Java 8以降ではネイティブメモリを使用する “Metaspace” が導入され、以前存在したPermGen(Permanent Generation)は廃止された

特徴:

  • クラスファイルを読み込み、バイトコードを内部的に解析してできた「クラスのメタデータ」を保持
  • Metaspaceはデフォルトで拡張可能。-XX:MetaspaceSize などのオプションで管理する
  • クラスローダがアンロードされる(不要になれば)クラスも破棄されるが、通常の開発ではあまり頻繁にアンロードされないケースが多い

Java 7以前のPermGenでは「クラス情報」だけでなく「staticフィールドの実体も置かれる」といった説明がされることも多かったですが、Java 8以降は基本的にクラスメタデータをMetaspaceへ置く方針に変わっています。静的フィールド自体はヒープに置かれる実装が一般的です。

(広義の) Static領域

C/C++とは異なり、Javaでは「static領域」という名前の明確な領域は仕様上存在しません。しかし以下のようなものを“Static領域”とまとめて呼ぶことがあります。

  • static フィールド(クラス変数): クラスに1つだけ存在し、インスタンス化しなくてもアクセスできるフィールド
  • static メソッド: インスタンスに依存せず、クラス名から直接呼び出せるメソッド

この静的な要素がクラスロード時にどこへ格納されるかはJVMの実装依存です。しばしば「Method Areaに格納される」として説明されますが、実装によっては「staticフィールドはヒープ上に確保され、メタデータはMetaspaceに持つ」というケースが多いです。大まかに「クラスの初期化タイミングで確保されるクラス変数」くらいに理解しておくとよいでしょう。

まとめ Javaのメモリ管理

  1. スタック
    • メソッド呼び出し単位でpush/popされるため、局所的かつ高速なメモリ領域。
    • ローカル変数やパラメータなどが積まれ、メソッド終了時に自動で破棄。
  2. ヒープ
    • new で生成されるオブジェクトが格納される領域。
    • GCが不要オブジェクトを回収するので、プログラマは手動解放不要。
  3. メタスペース (Method Area)
    • クラスロードで得られたメタデータ、メソッドのバイトコード、JITコンパイル済みコードなどを格納。
    • Java 8以降はネイティブメモリを使用、PermGenの問題を解消。
  4. (広義の) Static領域
    • staticフィールドやstaticメソッドの情報がどこに置かれるかは実装依存。
    • 一般的にはフィールド実体はヒープ、クラス定義情報はメタスペースにある。

Javaプログラムの実行の流れを「メモリ」に焦点を当てて

ここからは、コンパイル後の .class ファイルがどのように読み込まれ、メモリに配置され、実際のメソッド呼び出しに至るまでの流れを順を追って見ていきます。

これまで説明してきたそれぞれのメモリがどのように利用されるのか?に焦点を当てて確認していきましょう。

1 クラスファイルのロード

  1. クラスローダの探索
    • Javaプログラムが「あるクラスを使うぞ」となったとき、まず「そのクラスは既にロード済みか?」を確認します。ロードされていなければ、ClassLoaderがクラスパスやJARファイルから .class を探します。
  2. クラスファイルの読み込み (バイナリ取得)
    • 見つかった .class ファイルのバイナリデータを読み込む。
    • この時点では、ファイル内容を単純にバイト列として保持しているだけです。

2 メタスペースへの格納とリンク

  1. クラスファイルの解析 (ロード・リンク)
    • .class のバイナリを解釈し、クラスのメタ情報(フィールド定義、メソッド定義、定数プールなど)を JVM 内部で扱いやすい構造体に変換します。
    • ここでバイトコード検証や外部クラス参照の解決などが行われ、クラスとして適切に利用可能かチェックされます。
  2. メタスペース(Metaspace) への配置
    • 解析した結果生まれるクラスメタデータがMetaspaceに格納されます。
    • Java 8以降では、PermGenの代わりにMetaspaceが使われ、ネイティブメモリ上に確保されるため、必要に応じてメモリを拡張できる仕組みになっています。

3 クラス初期化 (static領域)

  1. クラス初期化条件
    • ロードが完了しただけでは「クラスは未初期化」の状態です。
    • 実際にnewされたりstaticメンバに初アクセスするときに、初期化処理がトリガーされます。
  2. staticフィールドのメモリ確保 & 初期化子の実行
    • static 変数を保持する領域が確保され、static { ... } で定義された初期化処理が呼ばれます。
    • フィールドの実体はヒープに確保され、クラスメタデータはMetaspaceにある、という実装が多いです。

4 メソッド呼び出しとスタックフレーム

  1. メソッドのバイトコード取得 & 実行
    • あるクラスのメソッドが呼び出されると、JVMはまずMetaspace内の「メソッドのバイトコード情報」を参照します。
    • Interpreter(またはJITコンパイラ)を通じて、このバイトコードが実行されます。
  2. スタックフレーム生成
    • スレッドごとのJavaスタックに、新たなフレームが積まれます。
    • メソッド内で宣言されるローカル変数や引数、演算用の一時値はこのフレームに格納される。
  3. ヒープ上のオブジェクト参照
    • メソッド内でnewを呼ぶと、ヒープからオブジェクト用のメモリが割り当てられる。
    • スタック側には、そのオブジェクトの参照(ポインタ)が置かれる。
  4. メソッド終了時にスタックフレーム破棄
    • returnなどでメソッドを抜けると、対応するスタックフレームがポップされて破棄される。
    • ヒープ上のオブジェクトは参照が残っていれば生存し続ける。
タイトルとURLをコピーしました