# JVM内存规范上篇
作者:Ethan.Yang
博客:https://blog.ethanyang.cn (opens new window)
相关源码参考: https://github.com/YangYingmeng/_014JVM (opens new window)
承接《JVM入门》,本文聚焦 常量池、运行时数据区 与 对象内存布局。 目标是:把概念讲清、把边界讲准、把现象讲明白。
# 一、常量池全景:编译期 → 运行期 → 字符串池
# 1)编译期常量池
- 位置:
.class文件内部。 - 生成:编译阶段由
javac写入。 - 内容:字面量(数字、字符串等)与 符号引用(类/方法/字段的全限定名)。
- 作用:为 类加载后的解析 提供素材。
# 2)运行时常量池
- 位置:方法区/元空间(JDK8+ 在 Metaspace)。
- 来源:类加载时,把
.class的常量池搬进来,形成每个类独立的运行时常量池。 - 特点:不仅承载编译期常量,还支持运行期生成的常量/符号(如
invokedynamic、反射、String#intern触发的引用等)。
# 3)字符串常量池
- 位置:堆(JDK7 起移至堆)。
- 机制:
String#intern()保证同值字符串的池化唯一性(池中只保留一份引用)。
编译期 JVM加载期/运行期
┌──────────────┐ load ┌────────────────┐
│ .class 常量池 │ ───────────────▶ │ 运行时常量池 │
└──────────────┘ └────────────────┘
│
intern() / 运行期创建引用
▼
┌──────────────┐
│ 字符串常量池 │(堆)
└──────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
举个例子:
String a = "a"; // a 指向堆上的“字符串常量池”中已有的 "a"
String b = "a"; // b 与 a 指向同一池中对象
System.out.println(a == b); // true
String x = new String("a"); // 一般会:复用池中字面量 "a" 的对象引用
String y = new String("a"); // 再 new 一个独立实例
System.out.println(x == y); // false(两个堆对象不同)
String s1 = new String("a").intern();
String s2 = "a";
System.out.println(s1 == s2); // true(都指向池中同一对象)
2
3
4
5
6
7
8
9
10
11
12
13
字面量直接进池;
new String(literal)额外再造一个堆对象;intern()拿到池中唯一引用。
# 二、运行时数据区

# 1)方法区 / 元空间
作用:存储类元数据(Klass)、运行时常量池、静态变量、JIT 代码等。
异常:内存不足可能抛
OutOfMemoryError(如 Metaspace OOM)。与上篇关联:类加载时,
.class→ Klass(元数据) 就落在这里。
# 2)堆
作用:存放所有对象实例与数组,线程共享。
关键联系:
类加载完成后,JVM 在堆中创建一个
java.lang.Class对象,作为 Java 层访问 Klass 的入口;普通对象的头部含 Klass Pointer,直指类元数据,支持字段定位、方法分派。

# 3)Java 虚拟机栈
线程私有,维护方法调用的执行状态。
基本单位:栈帧
每个被线程调用的方法都会在栈中创建一个栈帧,方法执行时入栈,执行结束后出栈。
栈帧中包含
- 局部变量表(方法参数、局部变量)
- 操作数栈(指令运行时的操作数临时区)
- 动态链接(指向当前方法的常量池引用)
- 返回地址与附加信息
父/子线程如何“交互”?
当主线程(如
main方法所在线程)中创建子线程时,JVM 会为子线程分配一个新的虚拟机栈(独立于父线程)。父子线程之间不会通过栈直接通信,而是通过 堆内存中的共享对象(如队列、集合、Future、锁等) 实现协作、数据交互或同步。
线程间共享数据都存在堆中,而栈只是保存各自的执行状态和局部变量。
举例:
static class TestClass {
}
public static void main(String[] args) {
TestClass testClass = new TestClass();
void A(testClass) {
new Thread(new Runnable() {
@Override
public void run() {
void B(testClass);
}
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14

# 4)程序计数器
- 每个线程一个,用来记录下一条将执行指令的地址。
- 执行
native方法时,该计数器值未定义(实现上可视为空)。
# 5)本地方法栈
- 执行
native方法所用的栈空间。 - 当 Java 方法调用到 native,栈帧会与本地方法栈发生链接。

# 三、Java 对象:从头到脚的内存布局
一个 Java 对象通常分为三段:对象头(Header)、实例数据(Fields)、对齐填充(Padding)。
# 1)对象头
Mark Word
随状态变化存放不同信息
常见用途:
- 对象哈希码(identity hash code)。
- GC 分代年龄(YGC 复制次数, 4位 15次)。
- 锁信息(偏向锁、轻量级锁、重量级锁)。
- 标记位(是否为偏向锁、是否被 GC 标记等)。
Klass Pointer
指向方法区/元空间中的 Klass 元数据结构。
Klass 是 JVM 内部的 C++ 结构,记录了该类的完整信息:
- 类名、父类、接口
- 字段表(名称、类型、偏移量)
- 方法表(方法字节码入口、虚方法表 vtable)
- 常量池引用
JVM 执行时,通过 Klass 指针找到对象对应的类信息,从而支持 方法调用、字段访问、类型检查。
这就是为什么 对象可以“知道”它是哪个类创建的。
数组长度(仅数组对象有)
- 记录数组大小。
# 2)实例数据
存字段值(含父类字段 → 子类字段)。
存储顺序:
- 先父类字段,再子类字段。
- JVM 会根据 字段类型和平台要求进行字节对齐(减少内存碎片,保证高效访问)。
例如:
class A { int a; byte b; } class B extends A { long c; }1
2内存布局会先放
a、b,再放c,同时插入必要的 padding 保证对齐。
# 3)对齐填充
- JVM 要求对象大小必须是 8 字节的倍数(HotSpot 实现)。
- 如果实例数据大小不是 8 的倍数,会用空字节填充对齐,避免 CPU 访问效率低下。
# 对象与类元数据的关系
- 对象实例在堆中,头部有 Klass Pointer → 指向 Klass 元数据。
- Klass 元数据在方法区 / 元空间(Metaspace),里面有类的方法表、字段表、常量池引用等。
Class对象同样在堆中,内部有指针指向 Klass,封装了反射 API,供 Java 代码使用。
JVM 执行方法、访问字段时,其实直接通过对象头里的 Klass Pointer → Klass 来找到方法入口或字段偏移量。 而调用
obj.getClass()或clazz.getDeclaredMethods(),走的则是 Class 对象 → Klass 的反射通路。
堆内存(对象实例)
┌───────────────────────────────┐
│ Object Header │
│ - Mark Word │
│ - Klass Pointer ──────────────▶ 方法区/元空间(Klass 元数据)
│ - [Array Length] (可选) │
├───────────────────────────────┤
│ Instance Data (字段值) │
│ - 父类字段 │
│ - 本类字段 │
├───────────────────────────────┤
│ Padding (8字节对齐填充) │
└───────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
# 四、三类常见“指针关系”
# 1)栈 → 堆
- 局部变量表里保存的是引用,指向堆中的对象/数组/
Class对象。
# 2)方法区/元空间 → 堆
类的静态字段若是引用类型,其值是一个指向堆对象的引用。
private static Object obj = new Object();1
# 3)堆 → 方法区/元空间
- 通过对象头中的 Klass 指针
当使用
new创建对象时,JVM 会在堆中分配内存,并在对象头中写入一个 Klass 指针,它直接指向方法区(元空间)中对应类的元数据结构。这样对象就“知道”自己属于哪个类,从而支持字段查找、方法分派等。 - 通过反射机制获取 Class 对象
当在代码中调用
obj.getClass()或通过反射 API 获取类信息时,JVM 会返回一个在堆中创建的Class对象。这个Class对象内部也持有指向方法区元数据的 native 指针,用于支撑反射等动态功能。
因此,堆中的对象无论是通过 Klass 指针(运行时底层机制),还是通过 Class 对象(Java 层面反射机制),都可以访问到方法区中的类元数据。