実行時データ領域
Javaプログラムの実行中、JVMは管理するメモリを複数の異なるデータ領域に分割します。これらの領域はそれぞれ特定の用途を持ち、作成と破棄のタイミングが異なります。一部の領域は仮想マシンプロセスの起動時に存在し続けますが、他の領域はユーザースレッドの開始と終了に依存して構築および破棄されます。
プログラムカウンタ:現在のスレッドが実行する次のバイトコード命令の行番号を記録するために使用され、スレッドと同時に作成および破棄されます。
Java仮想マシンスタック
プログラムカウンタと同様に、Java仮想マシンスタック(Java Virtual Machine Stack)もスレッド専有です。
メソッドが呼び出されるたびに、対応するスタックフレームがスタックにプッシュされます。これはメソッドをスタックフレームに変換するのではなく、メソッド内で定義された変数、パラメータ、メソッドの出口をスタックフレームのローカル変数表に配置することを意味します。
ローカル変数表には、コンパイル時にわかるJava仮想マシンの基本データ型(boolean、byte、char、short、int、float、long、double)、オブジェクト参照(reference型、これはオブジェクト自体ではなく、オブジェクトの開始アドレスを指す参照ポインタである可能性があります)、およびreturnAddress型(バイトコード命令のアドレスを指します)が格納されます。これらのデータ型は、ローカル変数スロット(Slot)で表されるストレージスペースを使用してローカル変数表に格納されます。
64ビットのデータ型は2つの連続した変数スロットを占め、その他のデータ型は1つの変数スロットを占めます。
ローカル変数に必要なメモリスペースはコンパイル時に確定します。基本データ型は値を直接格納し、参照型は固定長のポインタまたはハンドル(参照型の位置を見つけることができるもの)を格納します。
Java仮想マシン仕様では、このメモリ領域に対して2種類の例外状況が定義されています:スレッドが要求するスタックの深さが仮想マシンが許可する深さを超える場合、StackOverflowError例外がスローされます;Java仮想マシンスタックの容量が動的に拡張できる場合、スタックの拡張時に十分なメモリを確保できないとOutOfMemoryError例外がスローされます。
ネイティブメソッドスタック
ネイティブメソッドスタック(Native Method Stacks)は仮想マシンスタックと非常に似た機能を発揮しますが、違いは仮想マシンスタックが仮想マシンが実行するJavaメソッド(バイトコード)をサービスし、ネイティブメソッドスタックが仮想マシンが使用するネイティブ(Native)メソッドをサービスすることです。
ネイティブメソッドスタックの使用言語、使用方法、データ構造に特別な制限はありません。そのため、自由に実装でき、一部のJava仮想マシン(HotSpot仮想マシンなど)はネイティブメソッドスタックと仮想マシンスタックを統合しています。
Javaヒープ
すべてのスレッドが共有する領域で、オブジェクトにアクセスするために使用されます。ほとんどのオブジェクトはヒープ内に存在します。
メモリ割り当ての観点から見ると、すべてのスレッドが共有するJavaヒープ内では、オブジェクト割り当ての効率を向上させるために複数のスレッド専有の割り当てバッファ(Thread Local Allocation Buffer、TLAB)を分割できます。
Javaヒープは物理的に連続していないメモリスペースですが、論理的に連続しているかどうかは問いません。
Javaヒープを細分化する目的は、メモリをより効率的に回収するためです。
Javaヒープにメモリが不足してインスタンスの割り当てを完了できず、ヒープもさらに拡張できない場合、Java仮想マシンはOutOfMemoryError例外をスローします。
メソッド領域
メソッド領域(Method Area)はJavaヒープと同様にすべてのスレッドが共有するメモリ領域で、仮想マシンによってロードされた型情報、定数、静的変数、即时コンパイラによってコンパイルされたコードキャッシュなどのデータを格納するために使用されます。
実行時定数プール
コンパイル中に生成されるさまざまなシンボルとシンボル参照。クラス情報、メソッド、生成されたclass、final、静的属性などを格納し、スレッド間で共有されます。
Java仮想マシンはClassファイルの各部分(もちろん定数プールを含む)の形式に厳密な規定をしていますが、実行時定数プールについては、詳細な要件はありません。異なるプロバイダーが実装した仮想マシンは、このメモリ領域を独自の要件に応じて実装できますが、一般的には、Classファイルで記述されたシンボル参照を保存するだけでなく、シンボル参照から翻訳された直接参照も実行時定数プールに格納します。実行時定数プールはClassファイルの定数プールに対して、動的性という重要な特徴もあります。Classファイルの定数プールに事前に格納されている内容だけがメソッド領域の実行時定数プールに入るわけではなく、実行中にも新しい定数をプールに追加できます。
実行時定数プールはメソッド領域の一部であるため、メソッド領域のメモリ制限の影響を受けます。定数プールがさらにメモリを確保できない場合、OutOfMemoryError例外がスローされます。
直接メモリ
仮想マシンの実行時データ領域の一部ではなく、メモリ領域でもありません。JVMに割り当てられたメモリではなく、物理マシンが管理するメモリが直接メモリです。
チャネル(Channel)とバッファ(Buffer)に基づくI/O方式が導入され、ネイティブ関数ライブラリを使用してヒープ外メモリを直接割り当て、Javaヒープ内に格納されたDirectByteBufferオブジェクトを介してこのメモリを操作できます。
ネイティブ直接メモリの割り当てはJavaヒープサイズの制限を受けませんが、メモリである以上、物理メモリ(物理メモリ、SWAPパーティションまたはページファイルを含む)の総量およびプロセッサのアドレス空間の制限の影響を受けます。
オブジェクトの作成
Javaヒープ内のメモリが完全に整頓されていると仮定し、すべての使用済みメモリが一方に配置され、空きメモリがもう一方に配置され、その間にポインタが境界点の指示器として配置されている場合、メモリの割り当ては単にポインタを空きスペース方向にオブジェクトのサイズと等しい距離だけ移動させるだけで済みます。この割り当て方法は「ポインタ衝突」(Bump The Pointer)と呼ばれます。しかし、Javaヒープ内のメモリが整頓されていない場合、使用済みメモリと空きメモリが互いに混在していると、単純なポインタ衝突はできません。仮想マシンは、どのメモリブロックが使用可能かを記録するリストを維持し、割り当て時にリストから十分な大きさのスペースを見つけてオブジェクトインスタンスに割り当て、リストの記録を更新する必要があります。この割り当て方法は「空きリスト」(Free List)と呼ばれます。どちらの割り当て方法を選択するかはJavaヒープが整頓されているかどうかによって決まりますが、Javaヒープが整頓されているかどうかは、使用するガベージコレクタがスペース圧縮整理(Compact)機能を持っているかどうかによって決まります。
問題:オブジェクトの作成は仮想マシン内で非常に頻繁に行われる操作です。ポインタが指す位置を単に変更するだけでも、同時実行環境下ではスレッドセーフではなく、オブジェクトAにメモリを割り当てている最中にポインタが変更される前に、オブジェクトBが元のポインタを使用してメモリを割り当ててしまう可能性があります。
この問題を解決するには2つの選択肢があります:1つはメモリ割り当てアクションを同期処理すること——実際には仮想マシンはCASと失敗再試行の方式を採用して更新操作の原子性を保証します;もう1つはメモリ割り当てアクションをスレッドに応じて異なるスペースに分割すること、つまり各スレッドがJavaヒープ内に事前に小さなメモリを割り当て、これをローカルスレッド割り当てバッファ(Thread Local Allocation Buffer、TLAB)と呼びます。どのスレッドがメモリを割り当てるかによって、そのスレッドのローカルバッファで割り当てます。ローカルバッファが使い切れた場合にのみ、新しいバッファを割り当てる際に同期ロックが必要になります。
メモリ割り当てが完了した後、仮想マシンは割り当てられたメモリスペース(オブジェクトヘッダを除く)をゼロ値で初期化する必要があります。TLABを使用している場合、この操作はTLABの割り当て時に一緒に行うこともできます。このステップにより、オブジェクトのインスタンスフィールドはJavaコードで初期値を設定せずに直接使用でき、プログラムはこれらのフィールドのデータ型に対応するゼロ値にアクセスできます。
オブジェクトのメモリレイアウト
オブジェクトはヒープメモリ内のストレージレイアウトを3つの部分に分けることができます:オブジェクトヘッダ(Header)、インスタンスデータ(Instance Data)、およびアラインメントパディング(Padding)。
オブジェクトヘッダには2種類の情報が含まれます。1つ目はオブジェクト自身の実行時データを格納するために使用されます、ハッシュコード(HashCode)、GC世代年齢、ロック状態フラグなどで、これらは「Mark Word」と呼ばれます。Mark Wordは動的に定義されたデータ構造として設計されており、できる限り多くのデータを小さなスペースに格納し、オブジェクトの状態に応じてストレージスペースを再利用します。オブジェクトヘッダのもう1つの部分は型ポインタで、オブジェクトがその型メタデータを指すポインタです。Java仮想マシンはこのポインタを使用して、オブジェクトがどのクラスのインスタンスであるかを判断します。
オブジェクトがJava配列の場合、オブジェクトヘッダには配列の長さを記録するためのデータブロックが必須です。仮想マシンは通常のJavaオブジェクトのメタデータ情報からJavaオブジェクトのサイズを判断できますが、配列の長さが不明な場合、メタデータの情報から配列のサイズを推測できません。
インスタンスデータ部分はオブジェクトが実際に格納する有効な情報で、プログラムコードで定義されたさまざまな型のフィールド内容が記録されます。親クラスから継承されたフィールドでも、子クラスで定義されたフィールドでも記録する必要があります。HotSpot仮想マシンのデフォルトの割り当て順序はlongs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers、OOPs)で、デフォルトの割り当て戦略からわかるように、同じ幅のフィールドは常に一緒に配置されます。この前提条件を満たす場合、親クラスで定義された変数が子クラスの前に表示されます。
アラインメントパディングには特別な意味はありません。単なるプレースホルダの役割を果たします。HotSpot仮想マシンの自動メモリ管理システムは、オブジェクトの開始アドレスが8バイトの整数倍でなければならないことを要求します。言い換えれば、どのオブジェクトのサイズも8バイトの整数倍でなければなりません。オブジェクトヘッダ部分はすでに8バイトの倍数(1倍または2倍)に設計されているため、オブジェクトのインスタンスデータ部分がアラインされていない場合、アラインメントパディングで補完する必要があります。
オブジェクトのアクセスと位置決定
Javaプログラムはスタック上のreferenceデータを介してヒープ上の具体的なオブジェクトを操作します。reference型はオブジェクトへの参照であることを規定していますが、この参照がヒープ内のオブジェクトの具体的な位置にアクセスする方法を定義していないため、オブジェクトのアクセス方法は仮想マシンの実装によって決まります。主流のアクセス方法には、ハンドルを使用する方法と直接ポインタを使用する方法の2種類があります。
ハンドルアクセス:Javaヒープ内にハンドルプールとしてメモリを割り当て、referenceにはオブジェクトのハンドルアドレスが格納されます。ハンドルには、オブジェクトインスタンスデータと型データのそれぞれの具体的なアドレス情報が含まれます。
直接ポインタアクセス:Javaヒープ内のオブジェクトのメモリレイアウトは、型データへのアクセス情報をどのように配置するかを考慮する必要があります。referenceにはオブジェクトアドレスが直接格納され、オブジェクト自体にアクセスするだけであれば、余分な間接アクセスのオーバーヘッドはありません。
ハンドルアクセスの最大の利点:referenceに格納されているのは安定したハンドルアドレスで、オブジェクトが移動(ガベージコレクション時にオブジェクトを移動することは非常に一般的な動作)された場合、ハンドル内のインスタンスデータポインタのみが変更され、reference自体は変更する必要がありません。欠点:データにアクセスする際、ハンドルアクセスは2回のアクセスが必要です。
直接ポインタアクセスの利点:速度が速い、ポインタの位置決定にかかる時間のオーバーヘッドを1回節約します。オブジェクトへのアクセスはJavaで非常に頻繁に行われるため、仮想マシンは直接ポインタを採用しています。
実践:メモリオーバーフロー例外のテスト
Javaヒープオーバーフロー
/**
* VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapMemoryTest {
static class MemoryObject {
}
public static void main(String[] args) {
List<MemoryObject> objectList = new ArrayList<>();
while (true) {
objectList.add(new MemoryObject());
}
}
}
仮想マシンスタックとネイティブメソッドスタックのオーバーフロー
HotSpot仮想マシンがOutOfMemoryError例外を生成するかどうかをテストするために、以下の2つの動作を試してみましょう:
1. -Xssパラメータを使用してスタックメモリ容量を減少させます。
結果:StackOverflowError例外がスローされ、例外が発生したときに表示されるスタックの深さが相応に縮小されます。
/**
* VM Args:-Xss128k
*/
public class StackOverflowTest {
private int depth = 1;
public void recursiveMethod() {
depth++;
recursiveMethod();
}
public static void main(String[] args) throws Throwable {
StackOverflowTest test = new StackOverflowTest();
try {
test.recursiveMethod();
} catch (Throwable e) {
System.out.println("Stack depth: " + test.depth);
throw e;
}
}
}
2. 多数のローカル変数を定義して、メソッドフレーム内のローカル変数表の長さを増やします。
結果:StackOverflowError例外がスローされ、例外が発生したときに表示されるスタックの深さが相応に縮小されます。
public class LargeLocalVariablesTest {
private static int counter = 0;
public static void testMethod() {
long var1, var2, var3, var4, var5, var6, var7, var8, var9, var10,
var11, var12, var13, var14, var15, var16, var17, var18, var19, var20,
var21, var22, var23, var24, var25, var26, var27, var28, var29, var30,
var31, var32, var33, var34, var35, var36, var37, var38, var39, var40,
var41, var42, var43, var44, var45, var46, var47, var48, var49, var50,
var51, var52, var53, var54, var55, var56, var57, var58, var59, var60,
var61, var62, var63, var64, var65, var66, var67, var68, var69, var70,
var71, var72, var73, var74, var75, var76, var77, var78, var79, var80,
var81, var82, var83, var84, var85, var86, var87, var88, var89, var90,
var91, var92, var93, var94, var95, var96, var97, var98, var99, var100;
counter++;
testMethod();
var1 = var2 = var3 = var4 = var5 = var6 = var7 = var8 = var9 = var10 =
var11 = var12 = var13 = var14 = var15 = var16 = var17 = var18 = var19 = var20 =
var21 = var22 = var23 = var24 = var25 = var26 = var27 = var28 = var29 = var30 =
var31 = var32 = var33 = var34 = var35 = var36 = var37 = var38 = var39 = var40 =
var41 = var42 = var43 = var44 = var45 = var46 = var47 = var48 = var49 = var50 =
var51 = var52 = var53 = var54 = var55 = var56 = var57 = var58 = var59 = var60 =
var61 = var62 = var63 = var64 = var65 = var66 = var67 = var68 = var69 = var70 =
var71 = var72 = var73 = var74 = var75 = var76 = var77 = var78 = var79 = var80 =
var81 = var82 = var83 = var84 = var85 = var86 = var87 = var88 = var89 = var90 =
var91 = var92 = var93 = var94 = var95 = var96 = var97 = var98 = var99 = var100 = 0;
}
public static void main(String[] args) {
try {
testMethod();
} catch (Error e) {
System.out.println("Stack depth: " + counter);
throw e;
}
}
}
スレッド作成によるメモリオーバーフロー例外
/**
* VM Args:-Xss2M (32ビットシステムで実行してください)
*/
public class ThreadMemoryTest {
private void infiniteLoop() {
while (true) {
}
}
public void createThreads() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
infiniteLoop();
}
});
thread.start();
}
}
public static void main(String[] args) throws Throwable {
ThreadMemoryTest test = new ThreadMemoryTest();
test.createThreads();
}
}
実行時定数プールによるメモリオーバーフロー例外
/**
* VM Args:-XX:PermSize=6M -XX:MaxPermSize=6M
*/
public class ConstantPoolTest {
public static void main(String[] args) {
Set<String> constantSet = new HashSet<>();
short index = 0;
while (true) {
constantSet.add(String.valueOf(index++).intern());
}
}
}
メソッド領域によるメモリオーバーフロー例外
/**
* VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class MethodAreaTest {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Object.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
}
直接メモリによるメモリオーバーフロー例外
/**
* VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
*/
public class DirectMemoryTest {
private static final int ONE_MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(ONE_MB);
}
}
}