JVM内存模型详解
·
JVM内存模型详解
一、JVM内存结构总览
┌─────────────────────────────────────────────────────────────────┐
│ JVM 运行时数据区 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ================== 【线程共享区域】=================== │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────┐ │ │
│ │ │ 方法区 (Method Area/MetaSpace) │ │ │
│ │ │ ┌───────────────────────────────────────────┐ │ │ │
│ │ │ │ - 类信息 │ │ │ │
│ │ │ │ - 常量池 │ │ │ │
│ │ │ │ - 静态变量 │ │ │ │
│ │ │ └───────────────────────────────────────────┘ │ │ │
│ │ └───────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────┐ │ │
│ │ │ 堆内存 (Heap) │ │ │
│ │ │ ┌───────────────────────────────────────────┐ │ │ │
│ │ │ │ 新生代 │ │ │ │
│ │ │ │ - Eden 区 │ │ │ │
│ │ │ │ - S0 区 │ │ │ │
│ │ │ │ - S1 区 │ │ │ │
│ │ │ ├───────────────────────────────────────────┤ │ │ │
│ │ │ │ 老年代 │ │ │ │
│ │ │ │ - 存活时间长的对象 │ │ │ │
│ │ │ └───────────────────────────────────────────┘ │ │ │
│ │ └───────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────┐ │ │
│ │ │ 直接内存 (Direct Memory) │ │ │
│ │ │ - NIO操作使用的内存 │ │ │
│ │ └───────────────────────────────────────────────────┘ │ │
│ │ │ │
│ ============================================================= │
│ │
│ ================== 【线程私有区域】=================== │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────┐ │ │
│ │ │ 程序计数器 (PC Register) │ │ │
│ │ │ - 存放当前执行指令地址 │ │ │
│ │ └───────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────┐ │ │
│ │ │ 虚拟机栈 (JVM Stack) │ │ │
│ │ │ ┌───────────────────────────────────────────┐ │ │ │
│ │ │ │ 线程1: │ │ │ │
│ │ │ │ - 栈帧1 (方法1) │ │ │ │
│ │ │ │ - 栈帧2 (方法2) │ │ │ │
│ │ │ │ - ... │ │ │ │
│ │ │ ├───────────────────────────────────────────┤ │ │ │
│ │ │ │ 线程2: │ │ │ │
│ │ │ │ - 栈帧1 (方法1) │ │ │ │
│ │ │ │ - 栈帧2 (方法2) │ │ │ │
│ │ │ │ - ... │ │ │ │
│ │ │ └───────────────────────────────────────────┘ │ │ │
│ │ └───────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────┐ │ │
│ │ │ 本地方法栈 (Native Method Stack) │ │ │
│ │ │ - 为Native方法服务 │ │ │
│ │ └───────────────────────────────────────────────────┘ │ │
│ │ │ │
│ ============================================================= │
│ │
└─────────────────────────────────────────────────────────────────┘
二、各区域详细说明
1. 堆内存 (Heap) - 线程共享
作用: 存放对象实例和数组
特点:
- JVM中最大的一块内存区域
- 被所有线程共享
- 垃圾回收的主要区域
示例代码:
public class HeapDemo {
public static void main(String[] args) {
// 对象1:存储在堆中
User user1 = new User("张三", 25);
// 对象2:存储在堆中
User user2 = new User("李四", 30);
// 数组:存储在堆中
int[] numbers = {1, 2, 3, 4, 5};
// user1, user2, numbers 引用存储在栈中,指向堆中的对象
}
}
class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name; // "张三"存储在堆中
this.age = age; // 25存储在堆中
}
}
内存示意图:
栈内存 堆内存
┌─────────────────────┐ ┌────────────────────────┐
│ main方法栈帧 │ │ │
│ │ │ ┌──────────────────┐ │
│ user1 ──────────────┼───────▶│ │ User对象1 │ │
│ [0x1234] │ │ │ name: "张三" │ │
│ │ │ │ age: 25 │ │
│ user2 ──────────────┼───────▶│ └──────────────────┘ │
│ [0x5678] │ │ [0x1234] │
│ │ │ │
│ numbers ────────────┼───────▶│ ┌──────────────────┐ │
│ [0x9ABC] │ │ │ User对象2 │ │
│ │ │ │ name: "李四" │ │
└─────────────────────┘ │ │ age: 30 │ │
│ └──────────────────┘ │
│ [0x5678] │
│ │
│ ┌──────────────────┐ │
│ │ int[]数组 │ │
│ │ [1, 2, 3, 4, 5] │ │
│ └──────────────────┘ │
│ [0x9ABC] │
└────────────────────────┘
分代结构:
堆内存
├──────────────────────────────────┐
│ 老年代 (Old Generation) │ 存放长期存活的对象
│ │ 例如:缓存、单例对象
└──────────────────────────────────┘
├──────────────────────────────────┐
│ 新生代 (New Generation) │ 存放新创建的对象
│ ┌──────────────────────────┐ │ 例如:临时对象、方法局部变量
│ │ Eden 区 (80%) │ │
│ │ 新对象首先进入这里 │ │
│ └──────────────────────────┘ │
│ ┌──────────────┬─────────────┐ │
│ │ Survivor 0 │ Survivor 1 │ │ 经过GC后存活的对象
│ │ S0 (10%) │ S1 (10%) │ │ 在S0和S1之间复制
│ └──────────────┴─────────────┘ │
└──────────────────────────────────┘
2. 方法区 / 元空间 (Method Area / MetaSpace) - 线程共享
作用: 存储类信息、常量、静态变量
特点:
- Java 8及之前称为"永久代"
- Java 8之后改为"元空间",使用本地内存
示例代码:
public class MethodAreaDemo {
// 静态变量:存储在方法区
private static String STATIC_CONFIG = "config_value";
private static final int MAX_SIZE = 100;
private static final Map<String, String> CACHE = new HashMap<>();
// 类信息:存储在方法区
// - 类名、父类、接口
// - 方法的字节码
// - 字段信息
public static void main(String[] args) {
// 字符串常量:存储在方法区的字符串常量池
String s1 = "hello";
String s2 = "hello"; // s2和s1指向同一个字符串常量
System.out.println(s1 == s2); // true,指向同一对象
}
}
内存示意图:
方法区/元空间
┌─────────────────────────────────────────┐
│ 类信息 (Class Metadata) │
│ ┌─────────────────────────────────┐ │
│ │ MethodAreaDemo类 │ │
│ │ - 类名: MethodAreaDemo │ │
│ │ - 方法: main() │ │
│ │ - 字段: STATIC_CONFIG │ │
│ └─────────────────────────────────┘ │
│ │
│ 静态变量 │
│ ┌─────────────────────────────────┐ │
│ │ STATIC_CONFIG = "config_value" │ │
│ │ MAX_SIZE = 100 │ │
│ │ CACHE = HashMap对象引用 │ │
│ └─────────────────────────────────┘ │
│ │
│ 字符串常量池 │
│ ┌─────────────────────────────────┐ │
│ │ "hello" ←──── s1 ─────┐ │ │
│ │ │ │ │
│ │ "config_value" │ │ │
│ │ │ │ │
│ │ "Java" │ │ │
│ └────────────────────────┘ │ │
│ │ │
└───────────────────────────────────┼────┘
│
栈内存 │
┌─────────▼────────┐
│ main方法栈帧 │
│ │
│ s1 ──────────────┘
│ │
│ s2 ──────────────┘
│ │
└──────────────────┘
3. 虚拟机栈 (JVM Stack) - 线程私有
作用: 存放方法调用、局部变量、操作数栈
特点:
- 每个线程都有独立的虚拟机栈
- 每个方法调用创建一个栈帧
- 方法执行结束栈帧弹出
示例代码:
public class StackDemo {
public static void main(String[] args) {
int a = 10;
int b = 20;
int result = add(a, b);
System.out.println(result);
}
private static int add(int x, int y) {
int sum = x + y;
return sum;
}
}
内存示意图:
虚拟机栈 (当前线程)
┌────────────────────────────────┐
│ ┌──────────────────────────┐ │
│ │ main方法栈帧 │ │
│ │ │ │
│ │ 局部变量表 │ │
│ │ ┌────────────┐ │ │
│ │ │ args = {} │ │ │
│ │ │ a = 10 │ │ │
│ │ │ b = 20 │ │ │
│ │ │ result = ? │ │ │
│ │ └────────────┘ │ │
│ │ │ │
│ │ 操作数栈 │ │
│ │ ┌────────────┐ │ │
│ │ │ [ ] │ │ │
│ │ └────────────┘ │ │
│ │ │ │
│ │ 返回地址: add方法 │ │
│ └──────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────┐ │
│ │ add方法栈帧 │ │
│ │ │ │
│ │ 局部变量表 │ │
│ │ ┌────────────┐ │ │
│ │ │ x = 10 │ │ │
│ │ │ y = 20 │ │ │
│ │ │ sum = 30 │ │ │
│ │ └────────────┘ │ │
│ │ │ │
│ │ 操作数栈 │ │
│ │ ┌────────────┐ │ │
│ │ │ 30 │ │ │ (x + y的结果)
│ │ └────────────┘ │ │
│ │ │ │
│ │ 返回地址: main方法 │ │
│ └──────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────┐ │
│ │ System.out.println栈帧 │ │
│ └──────────────────────────┘ │
└────────────────────────────────┘
栈帧结构:
每个栈帧包含:
┌────────────────────────────┐
│ 局部变量表 │ ← 方法参数、局部变量
│ ───────────────────────── │
│ 操作数栈 │ ← 方法执行过程中的数据
│ ───────────────────────── │
│ 动态链接 │ ← 指向运行时常量池
│ ───────────────────────── │
│ 返回地址 │ ← 方法返回后的执行位置
└────────────────────────────┘
4. 程序计数器 (PC Register) - 线程私有
作用: 存放当前执行的字节码指令地址
特点:
- 唯一一个没有内存溢出的区域
- 记录当前线程执行到哪一行字节码
示例说明:
public void example() {
int a = 10; // PC指向这条指令
int b = 20; // 执行后PC指向下一条
int c = a + b; // 继续指向这条指令
return; // 最后指向return指令
}
示意图:
线程1的程序计数器 线程2的程序计数器
┌─────────────────┐ ┌─────────────────┐
│ PC = 0x1234 │ │ PC = 0x5678 │
│ (执行main方法) │ │ (执行其他方法) │
└─────────────────┘ └─────────────────┘
字节码地址对应:
0x1234: aload_0 // 加载this
0x1235: bipush 10 // 压入10
0x1237: istore_1 // 存储到a
0x1238: bipush 20 // 压入20
0x123A: istore_2 // 存储到b
0x123B: iload_1 // 加载a
0x123C: iload_2 // 加载b
0x123D: iadd // a + b
0x123E: ireturn // 返回
5. 本地方法栈 (Native Method Stack) - 线程私有
作用: 为本地方法(native方法)服务
特点:
- 与虚拟机栈类似
- 但为Native方法服务
示例代码:
public class NativeDemo {
// 本地方法,调用C/C++代码
private native void nativeMethod();
// Object类中的native方法示例
// public native int hashCode();
// public final native Class<?> getClass();
public static void main(String[] args) {
NativeDemo demo = new NativeDemo();
demo.nativeMethod(); // 在本地方法栈中执行
}
}
内存示意图:
虚拟机栈 本地方法栈
┌──────────────────┐ ┌──────────────────┐
│ Java方法栈帧 │ │ Native方法栈帧 │
│ │ │ │
│ main() │ │ nativeMethod() │ ← 调用C代码
│ └─ nativeMethod()├────────┤ │
│ │ │ C函数调用栈 │
└──────────────────┘ │ - func1() │
│ - func2() │
└──────────────────┘
6. 直接内存 (Direct Memory) - 线程共享
作用: 避免在Java堆和Native堆之间复制数据
特点:
- 不受JVM堆内存限制
- 用于NIO操作
示例代码:
import java.nio.ByteBuffer;
public class DirectMemoryDemo {
public static void main(String[] args) {
// 分配直接内存
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024); // 1MB
// 这1MB不在Java堆中,而是在直接内存中
// 优点:减少Java堆和Native堆之间的数据复制
// 缺点:分配和释放成本较高
}
}
内存示意图:
JVM堆内存 直接内存
┌──────────────────┐ ┌──────────────────┐
│ Java对象 │ │ DirectBuffer │
│ │ │ │
│ ┌────────────┐ │ │ ┌────────────┐ │
│ │HeapBuffer │ │ │ │ 1MB数据 │ │
│ │(堆内缓冲) │ │ │ │ │ │
│ └────────────┘ │ │ └────────────┘ │
│ │ │ │
│ 需要复制数据 │ │ 直接访问 │
│ 到Native堆 │ │ 无需复制 │
└──────────────────┘ └──────────────────┘
↓ ↑
复制操作 零拷贝
三、完整示例:多线程场景
示例代码:
public class CompleteMemoryDemo {
// 静态变量:存储在方法区
private static final ConcurrentHashMap<String, String> CACHE =
new ConcurrentHashMap<>();
// 实例方法
public void processData(String input) {
// 局部变量:存储在栈中
String processed = input.toUpperCase();
// 创建对象:存储在堆中
User user = new User(processed);
// 对象引用存储在栈中,对象在堆中
CACHE.put(input, processed);
}
public static void main(String[] args) {
// 主线程
CompleteMemoryDemo demo = new CompleteMemoryDemo();
// 创建多个线程
for (int i = 0; i < 3; i++) {
final int index = i;
new Thread(() -> {
demo.processData("data-" + index);
}).start();
}
}
}
class User {
private String name;
public User(String name) {
this.name = name;
}
}
内存分布图:
JVM运行时数据区
┌─────────────────────────────────────────────────────┐
│ 方法区 │
│ ┌──────────────────────────────────────────────┐ │
│ │ CompleteMemoryDemo类信息 │ │
│ │ - 方法: processData(), main() │ │
│ │ - 字段: CACHE │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ CACHE = ConcurrentHashMap引用 │ │
│ │ (实际对象在堆中) │ │
│ └──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ 堆内存 │
│ ┌──────────────────────────────────────────────┐ │
│ │ ConcurrentHashMap实例 │ │
│ │ - "data-0" → "DATA-0" │ │
│ │ - "data-1" → "DATA-1" │ │
│ │ - "data-2" → "DATA-2" │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ User对象1 │ │ User对象2 │ │ User对象3 │ │
│ │ name="DATA │ │ name="DATA │ │ name="DATA │ │
│ │ -0" │ │ -1" │ │ -2" │ │
│ └────────────┘ └────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────┘
┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐
│ 主线程的虚拟机栈 │ │ 线程1的虚拟机栈 │ │ 线程2的虚拟机栈 │
│ ┌──────────────┐ │ │ ┌──────────────┐ │ │ ┌──────────────┐ │
│ │ main()栈帧 │ │ │ │ run()栈帧 │ │ │ │ run()栈帧 │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ demo ────────┼──┼──┼──▶│ processed │ │ │ │ processed │ │
│ │ │ │ │ │ index = 0 │ │ │ │ index = 1 │ │
│ │ for循环变量 │ │ │ │ user ────────┼──┼──┼──▶│ user ────────┼──┼────
│ │ i = 0,1,2 │ │ │ │ │ │ │ │ │ │
│ └──────────────┘ │ │ └──────────────┘ │ │ └──────────────┘ │
└────────────────────┘ └────────────────────┘ └────────────────────┘
┌────────────────────┐
│ 线程3的虚拟机栈 │
│ ┌──────────────┐ │
│ │ run()栈帧 │ │
│ │ │ │
│ │ processed │ │
│ │ index = 2 │ │
│ │ user ────────┼────┘
│ │ │ │
│ └──────────────┘ │
└────────────────────┘
每个线程都有独立的:
- 虚拟机栈
- 程序计数器
- 本地方法栈
但共享:
- 堆内存
- 方法区/元空间
四、EscapeObjectProcessor的缓存说明
为什么缓存的不是对象数据?
public class EscapeObjectProcessor {
// 这些缓存存储的是"反射元数据",不是"对象数据"
private static final ConcurrentHashMap<Class<?>, Field[]> FIELDS_CACHE = new ConcurrentHashMap<>(64);
private static final ConcurrentHashMap<Class<?>, List<FieldInfo>> ESCAPE_FIELDS_CACHE = new ConcurrentHashMap<>(64);
private static final ConcurrentHashMap<Class<?>, Boolean> JDK_TYPE_CACHE = new ConcurrentHashMap<>(128);
public void processObject(Object obj) {
if (obj == null) return;
// 1. 从缓存获取类的字段信息(元数据)
List<FieldInfo> fieldInfos = getEscapeFields(obj.getClass());
// 2. 遍历字段信息,处理每个对象的数据(动态读取)
for (FieldInfo fi : fieldInfos) {
Field field = fi.field;
field.setAccessible(true);
// 每次都从对象中读取当前值
String value = (String) field.get(obj);
// 处理值
String escaped = escapeProcessor.escape(value);
// 写入到对象中
field.set(obj, escaped);
}
}
}
内存分布图:
方法区/元空间 (线程共享)
┌──────────────────────────────────────────┐
│ FIELDS_CACHE (静态变量) │
│ ┌────────────────────────────────────┐ │
│ │ User.class ──▶ [name, age, email] │ │ ← 缓存的是"有哪些字段"
│ │ │ │
│ │ Order.class ──▶ [id, amount, time]│ │
│ │ │ │
│ │ Product.class ──▶ [name, price] │ │
│ └────────────────────────────────────┘ │
│ │
│ ESCAPE_FIELDS_CACHE (静态变量) │
│ ┌────────────────────────────────────┐ │
│ │ User.class ──▶ [name(带注解)] │ │ ← 缓存的是"哪些字段需要转义"
│ │ │ │
│ │ Order.class ──▶ [amount(带注解)] │ │
│ └────────────────────────────────────┘ │
│ │
│ EscapeObjectProcessor类信息 │
│ - processObject方法 │
│ - getEscapeFields方法 │
└──────────────────────────────────────────┘
堆内存 (线程共享)
┌──────────────────────────────────────────┐
│ ┌────────────┐ ┌────────────┐ │
│ │ User对象1 │ │ User对象2 │ │ ← 每个对象的数据是独立的
│ │ name="张三" │ │ name="李四" │ │
│ │ age=25 │ │ age=30 │ │
│ └────────────┘ └────────────┘ │
│ │
│ ┌────────────┐ ┌────────────┐ │
│ │ Order对象1 │ │ Order对象2 │ │
│ │ amount=100 │ │ amount=200 │ │
│ └────────────┘ └────────────┘ │
└──────────────────────────────────────────┘
虚拟机栈 - 线程1
┌──────────────────────────────────────────┐
│ processObject方法栈帧 │
│ ┌────────────┐ │
│ │ obj ───────┼────────────────────┐ │
│ │ │ │ │
│ │ fieldInfos ─┼────────────┐ │ │
│ └────────────┘ │ │ │
└─────────────────────────────│─────│──────┘
│ │
从缓存读取元数据 │ │ 从堆读取对象当前值
│ │
▼ ▼
┌─────────────────┐
│ 1. 从缓存获取 │
│ FieldInfo[] │
│ │
│ 2. field.get(obj) │ ← 动态读取对象数据
│ 读取当前值 │
│ │
│ 3. 处理数据 │
│ │
│ 4. field.set(obj) │ ← 动态写入对象数据
│ 写入处理结果 │
└─────────────────┘
虚拟机栈 - 线程2
┌──────────────────────────────────────────┐
│ processObject方法栈帧 │
│ ┌────────────┐ │
│ │ obj ───────┼──────────────────────┐ │
│ │ │ │ │
│ │ fieldInfos ─┼─────────────────┐ │ │
│ └────────────┘ │ │ │
└──────────────────────────────────┼───┼──┘
│ │
从同一个缓存读取│ │ 处理不同的对象
│ │
▼ ▼
┌───────────────┐
│ 处理不同的对象 │
│ 各自独立 │
└───────────────┘
关键点:
-
缓存是元数据:
FIELDS_CACHE存储的是"User类有哪些字段",不是"某个User对象的字段值是什么" -
数据是动态的:每次调用
field.get(obj)都会从堆内存中读取对象当前的值 -
线程安全:
- 每个线程有独立的虚拟机栈
- 每个方法的参数
obj是独立的(存储在各自线程的栈中) - 虽然多个线程可能同时读取同一个缓存,但缓存本身是只读的(获取后不会修改)
-
内存隔离:
线程1的obj ──▶ User对象1 (name="张三", age=25) 线程2的obj ──▶ User对象2 (name="李四", age=30) 两个线程共享: - FIELDS_CACHE (类的元数据) 但各自独立: - 自己的栈帧 - 自己的obj参数 - 各自处理不同的对象数据
五、常见问题
Q1: 为什么静态变量在方法区而不是栈?
A: 静态变量属于类,不属于方法。类的所有实例共享静态变量,所以放在线程共享的方法区。
Q2: 字符串常量为什么在方法区?
A: 为了避免重复创建相同的字符串,JVM在方法区维护一个字符串常量池,所有"hello"字面量都指向同一个对象。
Q3: 栈溢出和堆溢出有什么区别?
A:
- 栈溢出 (StackOverflowError): 递归太深,栈帧超过了虚拟机栈的深度限制
- 堆溢出 (OutOfMemoryError): 对象太多,堆内存不足
Q4: 为什么方法区改为元空间?
A: Java 8之前使用永久代,容易因为类加载过多导致内存溢出。改为元空间使用本地内存,可以动态调整,避免OOM。
Q5: 直接内存有什么用?
A: 主要用于NIO操作,可以避免在Java堆和Native堆之间复制数据,提高IO性能。
六、总结
| 内存区域 | 线程共享/私有 | 存储内容 | 常见异常 |
|---|---|---|---|
| 堆内存 | 共享 | 对象实例、数组 | OutOfMemoryError |
| 方法区 | 共享 | 类信息、常量、静态变量 | OutOfMemoryError |
| 虚拟机栈 | 私有 | 方法调用、局部变量 | StackOverflowError |
| 程序计数器 | 私有 | 当前执行指令地址 | 无 |
| 本地方法栈 | 私有 | Native方法调用 | StackOverflowError |
| 直接内存 | 共享 | NIO缓冲区 | OutOfMemoryError |
关键要点:
- 线程共享区域:堆内存、方法区、直接内存 - 需要考虑线程安全
- 线程私有区域:虚拟机栈、程序计数器、本地方法栈 - 天然线程安全
- 缓存策略:EscapeObjectProcessor缓存的是反射元数据(类的结构),不是对象数据(字段值)
- 参数隔离:每个方法调用有独立的栈帧,参数互不干扰
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)