🤖

Java仮想マシン(JVM)を読解しながら理解する #5

に公開

https://zenn.dev/h_kohe/articles/9eb8bde7884acc
こちらの記事の続きです。

よんでいる公式ドキュメント
https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-2.html

この2章だけでもまだまだ先のあるでなかかなの絶望を感じていますが。。。
以下の記事でJavaチャンピオンの1人でもある谷本さんが、

また、勉強会にもよく参加していました。中でも印象的だったのは、「Tomcatのコードを全部読むなんて普通だよね」と平然と話す人がいるような、レベルの高い勉強会があったことです。

と話していましたが、やはりすごいことが当然の世界線に身を置くのが大切なんやなと痛感したので
JVMのドキュメント読むのも長距離戦ですが懲りずに読み進めようと思います😇
https://findy-code.io/engineer-lab/cero_t

2.5.2. Java Virtual Machine Stacks(Java仮想マシンスタック)

JVMスタックとは??

  • 各Java仮想マシンスレッド には、そのスレッド専用の JVMスタック が割り当てられる
  • スレッドの生成と同時に作成され、スレッド終了時に破棄される
  • このスタックには フレーム(後続の2.6章で解説) が格納される

そもそもスタックとは??

「後入れ先出し(LIFO: Last In, First Out)」 のルールでデータを管理する仕組み

スタックの基本操作

  1. push : データをスタックに積む
  2. pop : スタックの一番上からデータを取り出す
  3. peek(top) : 今一番上にあるデータを確認する(取り出さない)

これだけでもなんとなくイメージはできていると思いますが、もう少しイメージしやすい具体例を・・・!!

ex)お皿のスタック

  • お皿を1枚ずつ重ねていく → push(積む)
  • 一番上のお皿から取る → pop(取り出す)

下のお皿を先に取ることはできず、必ず「最後に積んだものから取り出す」ルール

C言語などの従来の言語のスタックに類似しており、ローカル変数と部分的な結果を保持し、メソッドの呼び出しと戻り値の処理に使用されます。Java仮想マシンスタックは、フレームのプッシュとポップ以外では直接操作されることがないため、フレームはヒープ領域に割り当てられる場合があります。Java仮想マシンスタックのメモリは連続している必要はありません。

と記載があるのですが。。。
わかりそうでわからん。。。😇
なのでもう少し分解してみます

スタックとフレームの関係

  • JVMスタック = そのスレッド専用の「呼び出し履歴の箱」
  • フレーム = 1つのメソッド実行に必要な情報(ローカル変数、計算途中の値、戻り先アドレスなど)
    メソッドを呼び出すたびにフレームがスタックに積まれ(push)、メソッドが終了するとフレームが取り除かれる(pop) という仕組みである

「C言語などの従来の言語のスタックに類似している」とは?

  • C言語でも関数を呼ぶたびに「スタックフレーム」という領域が積まれる
  • Javaでも同じで、メソッド呼び出し = スタックフレームの積み上げ という構造を持っているので類似している

C言語は全く触ったことのない言語ですが、以下の記事がわかりやすく解説されていました。
https://qiita.com/uchidak/items/8efbf0190fb7f1e9bdda

スタックの基本操作

  • push : データをスタックに積む
  • pop : スタックの一番上からデータを取り出す
  • peek(top) : 今一番上にあるデータを確認する(取り出さない)

「フレームはヒープ領域に割り当てられる場合がある」とは?

  • JVMでは実装の自由度があるため必ずしも「連続したメモリ」でスタックを作る必要はない
  • フレームをヒープに置いて、リンクでつなげて“スタックのように扱う” 実装もOK
// main → callA → callX → 戻る → callB → callX → 戻る の順で処理される
public class Main {
    public static void main(String[] args) {
        callA();
        callB();
    }

    static void callA() {
        callX();
    }

    static void callB() {
        callX();
    }

    static void callX() {
        int x = 10;
    }
}

もう少し詳細に処理をの流れを追うと以下のようになります
1 :JVMスタックに Frame: main が push
2 :Frame: callA が push
3 :Frame: callX が push
4 :x=10 がローカル変数としてこのフレームに格納
5 :Frame: callX が消える
6 : callA() 終了 → pop
7 :Frame: callB が push
8 :再び Frame: callX が push(別呼び出しなので、さっきの callX のフレームとは完全に独立)
9 :x=10 がローカル変数としてこのフレームに格納
10:Frame: callX が消える
11:callB() 終了 → pop
12:main() 終了 → スタック空に

めっちゃ簡単な処理なので流れが読めて当然ですが・・・
この処理のなかでJVMが何をしているのかなんて考えたこともなかったので、いままで当然のように実装してきたJavaも裏でこんなスタックが積まれてpopされているとは。。。

まとめ

  • JVMスタックは「メソッドごとの作業台」を積み重ねる箱
  • 作業台(フレーム)には ローカル変数・途中計算・戻り先 が置かれる
  • 終わったら台を片付け(pop)、次の台に戻る
  • 再帰や深すぎる呼び出しが続くと、スタックにフレームが積みすぎて、許可されたスタックサイズを超えるとStackOverflowError
  • この作業台(フレーム)がどこに置かれるか(連続領域かヒープか)は JVM実装の自由

この勢いで続けたいのですが。。。
次の項目がヒープなのでまた長くなりそうな雰囲気満載のため来週に回します🙇

Discussion