JVMメモリ構造の概要
JVMのランタイムデータ領域はいくつかの部分に分かれますが、オブジェクトのデータを主に格納するのはヒープ領域とメソッド領域(非ヒープ領域)です。これらの領域はスレッド間で共有されます。一方、スタック(仮想マシンスタック、ネイティブメソッドスタック、プログラムカウンタ)は各スレッドごとに独立しています。
ヒープ領域は主に以下の部分で構成されます。まず、ヒープはOld領域とYoung領域に分かれます。Young領域はさらにEden領域とSurvivor領域(S0とS1)に分けられます。Survivor領域のS0とS1はサイズが同じで、FromとToとして機能します。
メモリ内でのオブジェクトの割り当てと回収
以下に、ヒープ、メソッド領域、スタックの各領域で発生する代表的なメモリ例外の例を示します。
ヒープ領域のOOM(Out of Memory)
ヒープ領域がオブジェクトでいっぱいになり、新しいオブジェクトを割り当てるスペースがなくなると、OOMエラーが発生します。以下の例では、無限ループでオブジェクトをリストに追加し続けることで、ヒープを枯渇させます。
// JVM起動オプション: -Xmx50M -Xms50M
public class HeapOOMExample {
public static void main(String[] args) {
java.util.List<Data> dataList = new java.util.ArrayList<>();
while (true) {
dataList.add(new Data());
}
}
}
class Data {
// 簡単なデータクラス
}
メソッド領域(メタスペース)のOOM
メソッド領域(Java 8以降はメタスペース)は、クラスやメソッドのメタデータを保持します。大量のクラスを動的に生成すると、この領域が枯渇し、OOMエラーが発生します。ASMライブラリを使用して多数のクラスを生成する例を示します。
// 依存関係 (Maven)
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.4</version>
</dependency>
// JVM起動オプション: -XX:MetaspaceSize=50M -XX:MaxMetaspaceSize=50M
import org.objectweb.asm.*;
import java.util.ArrayList;
import java.util.List;
public class ClassGenerator extends ClassLoader {
public static List> generateClasses() {
List> generatedClasses = new ArrayList<>();
for (int i = 0; i < 100000; ++i) {
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "GeneratedClass" + i, null, "java/lang/Object", null);
MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
methodVisitor.visitInsn(Opcodes.RETURN);
methodVisitor.visitMaxs(1, 1);
methodVisitor.visitEnd();
ClassGenerator loader = new ClassGenerator();
byte[] bytecode = classWriter.toByteArray();
Class> newClass = loader.defineClass("GeneratedClass" + i, bytecode, 0, bytecode.length);
generatedClasses.add(newClass);
}
return generatedClasses;
}
}
public class MetaspaceOOMExample {
public static void main(String[] args) {
java.util.List> classList = new java.util.ArrayList<>();
while (true) {
classList.addAll(ClassGenerator.generateClasses());
}
}
}
スタックオーバーフロー
スタックはメソッド呼び出しの情報を保持します。メソッドが再帰的に呼び出され続けると、スタックの深さが許容範囲を超え、スタックオーバーフローが発生します。
public class StackOverflowExample {
private static long callCount = 0;
public static void recursiveCall() {
System.out.println(callCount++);
recursiveCall(); // 無限再帰
}
public static void main(String[] args) {
recursiveCall();
}
}