プロジェクト

全般

プロフィール

Javaプログラミング 変数とメモリ

はじめに

Java読書会「Java Memory Management」を読む会実施を機に、Javaのメモリ管理を変数とメモリの観点で整理してみました。
書籍メモ: Java Memory Management

Javaの変数とメモリの概要

メモリの種類

Javaは実行時にJavaVMが次のメモリを用意し管理しています。

  • スタック
  • ヒープ
  • メタスペース
  • プログラムカウンタレジスタ
  • ネイティブメソッドスタック
+---------------------------------------------------+
| +------+ +------+ +-------+ +---------+ +-------+ |
| |Stack | |Heap  | |Method | |PC       | |Native | |
| |      | |      | |       | |registers| |method | |
| |      | |      | |       | |         | |stack  | |
| +------+ +------+ +-------+ +---------+ +-------+ |
+---------------------------------------------------+

スタックは、スレッド毎に独立したメモリが割り当てられ、ローカル変数と呼び出されたメソッド毎のフレームが置かれます。
ヒープは、インスタンスが置かれ、ガベージコレクタにより不要なメモリが自動で解放されます。
メタスペースはクラスの実行時表現が置かれ、メソッドの実行コード、クラス変数、コンスタントプールが含まれます。
プログラムカウンタレジスタは、スレッド毎に実行中の命令アドレスが保持されます。
ネイティブメソッドスタックは、C言語などのネイティブコードで書かれたプログラムコードが使用するスタックです。

変数の種類

変数の種類は、大まかに次に分類されます。

変数の種類 細分 値が置かれるメモリ
ローカル変数 プリミティブ型 スタックにプリミティブ値が格納
参照型 スタックにインスタンスを参照するポインタが格納
インスタンス変数 ヒープ
クラス変数 メタスペース

ローカル変数は、スタックに置かれます。詳しくは、メソッドが呼び出されると作られるメソッドフレームのなかのローカル変数配列に変数の値が置かれます。
プリミティブ型のローカル変数は、プリミティブ値が直接ローカル変数配列の中に置かれます。
参照型のローカル変数は、ヒープ上に置かれたインスタンスを参照するポインターがローカル変数配列の中に置かれます。
インスタンス変数は、インスタンス変数を抱えるインスタンスが割り当てられたヒープ上のメモリに置かれます。
クラス変数は、クラスが割り当てられたメタスペース上のメモリに置かれます。

メモリの構成

スタック内のメソッドフレーム

メソッドフレームは、スレッド毎に用意されるスタック上に置かれます。

+------------------------------------------------------------------------+
| +--------------------+ +--------------------+   +--------------------+ |
| | Stack for thread1  | | Stack for thread2  |   | Stack for thread N | |
| |                    | |                    |...|                    | |
| |                    | |+------------------+|   |                    | |
| |                    | ||Frame for method c||   |                    | |
| |                    | |+------------------+|   |                    | |
| |+------------------+| |+------------------+|   |                    | |
| ||Frame for method y|| ||Frame for method b||   |                    | |
| |+------------------+| |+------------------+|   |                    | |
| |+------------------+| |+------------------+|   |+------------------+| |
| ||Frame for method x|| ||Frame for method a||   ||Frame for method k|| |
| |+------------------+| |+------------------+|   |+------------------+| |
| +--------------------+ +--------------------+   +--------------------+ |
+------------------------------------------------------------------------+

あるメソッドが呼び出される時に、メソッドを実行するスレッドのスタック上にメソッドフレームが割り当てられます。
上図では実行中のスレッド毎にスタック領域が割り当てられ、スレッド1でメソッドxが呼び出され、メソッドxの中でメソッドyが呼び出されていることを示しています。同様にスレッド2でメソッドaが呼び出され、メソッドaの中でメソッドbが呼び出され、メソッドbの中でメソッドcが呼び出されていることを示しています。

メソッドフレームそれぞれには、次のデータが割り当てられます。

  • ローカル変数の配列
  • フレームデータ
  • オペランドスタック

ローカル変数の配列

ワードサイズ(32bit)の要素からなる配列で、ローカル変数の型によって1つまたは2つのスロットを使います。2つのスロットを使うのは、64bitの大きさであるlong型、double型です。

フレームデータ

メソッドの戻り先情報、例外発生に関する情報、コンスタントプールへの参照などが置かれます。(少し曖昧)

オペランドスタック

Javaコードを実行するときに使われるオペランドを格納するスタックです。Java命令はスタックマシンなので、計算に使う値などはオペランドスタックに置きます。

再帰呼び出しとメソッドフレーム

メソッドの呼び出し毎にメソッドフレームがスタック上に割り当てられるので、再帰呼び出しのように1つのスレッドで非常に多い実行中メソッドがある場合、スタックが枯渇する原因となります。そのようなプログラムを実行する場合は、JavaVMのオプションでスタックサイズをデフォルトから拡張しておくとよいでしょう。

デフォルトのスタックサイズの確認

javaコマンドに -XX:+PrintFlagsFinal オプションを指定して、StackSizeの項目を見ます。
次は、MacOS 12.6.1 OpenJDK 17のディストリビューション Liberia JDK 17 での実行結果です。

MacBook ~ % java -XX:+PrintFlagsFinal --version|grep StackSize
openjdk 17.0.7 2023-04-18 LTS
OpenJDK Runtime Environment (build 17.0.7+7-LTS)
OpenJDK 64-Bit Server VM (build 17.0.7+7-LTS, mixed mode, sharing)
  :
     intx CompilerThreadStackSize                  = 2048                                   {pd product} {default}
   size_t MarkStackSize                            = 4194304                                   {product} {ergonomic}
   size_t MarkStackSizeMax                         = 536870912                                 {product} {default}
     intx ThreadStackSize                          = 2048                                   {pd product} {default}
     intx VMThreadStackSize                        = 2048                                   {pd product} {default}

アプリケーションが使うスレッドのスタックサイズは、 ThreadStackSize です。デフォルトでは2048KBとなっています。

スレッドと変数

ローカル変数とスレッド

プリミティブ型のローカル変数

プリミティブ型のローカル変数の値は、実行中のメソッドのメソッドフレーム内にあるローカル変数配列に格納されています。この値は他のスレッドからはアクセスできないほか、他のメソッドフレーム(同じメソッドであっても別な流れで呼び出されている場合を含む)からもアクセスできません。
そのため、プリミティブ型のローカル変数はスレッド安全ですし、今実行しているメソッドの中からしか読み書きされません。

参照型のローカル変数

参照型のローカル変数は、参照先のインスタンスへのポインター値が、実行中のメソッドのメソッドフレーム内のローカル変数配列に格納されています。インスタンス自体のデータはヒープまたはメタスペースに存在しているので、他のメソッドのローカル変数、インスタンス変数、クラス変数から同じインスタンスを参照している可能性があります。
従って、インスタンスはスレッド安全ではありません。

インスタンス変数

インスタンス変数の値は、ヒープ上のインスタンスのデータに格納されます。
インスタンスのデータは、複数のスレッドから読み書きされるので、スレッド安全ではありません。

クラス変数

クラス変数の値は、メタスペース上のクラスデータに格納されます。
クラスデータは、複数のスレッドから読み書きされるので、スレッド安全ではありません。


9ヶ月前に更新