一步步问清 JVM 方法区、元空间、常量池、字符串常量池和 static 引用
从 x、S、"hello" 和局部变量 a 出发,一步步厘清 JVM 方法区、元空间、运行时常量池、字符串常量池与 static 引用的归属。
一步步问清 JVM 方法区、元空间、常量池、字符串常量池和 static 引用
很多人学 JVM 的方法区时,最容易卡住的不是“方法区是什么”,而是“一个变量、一个字符串、一个引用到底分别放哪”。我这次就按最小问题一步步问清楚,最后把它们拼成一张完整的内存图。
一、方法区里到底存什么?
先看这个例子:
class A {
static final String S = "hello";
int x = 1;
void f() {}
}
这里的方法区/元空间,主要放的是“类的信息”:
- 类名 A
- 字段 x 的信息
- 方法 f 的信息
- 静态字段 S 的信息
- 运行时常量池
它不是拿来放 new 出来的对象的。对象本体不在这里。
二、x 存哪,S 存哪?
x 是实例字段,它属于对象,所以 new A() 之后,x = 1 存在堆里的那个对象里。
S 是 static final 字段,它属于类,不属于某个对象,所以它跟着类走,不跟着对象走。
所以这两个东西的归属不同:
x:对象字段,跟对象走S:类字段,跟类走
三、常量池和字符串常量池不是一回事
这是最容易混的地方。
常量池可以理解成 .class 里的“表格”和“目录”,里面记着:
- 类名
- 字段名
- 方法名
- 字符串字面量
- 各种符号引用
它更像“索引”,不是对象本体。
字符串常量池只管字符串对象。比如 "hello" 这种字符串,JVM 会尽量复用:
- 如果池里已经有
"hello",就直接用 - 如果没有,就创建一个,再放进去
所以:
- 常量池:记名字、记引用线索
- 字符串常量池:管理字符串对象复用
四、static final String S = "hello" 到底怎么走
这行代码可以拆成三层看:
S这个字段属于类,跟类一起加载到方法区/元空间这一侧。"hello"这个字符串对象,通常在堆里。- 字符串常量池负责判断这个字符串能不能复用。
所以更准确地说:
S是类级别的静态字段S指向堆里的字符串对象"hello"会受字符串常量池管理
五、static 修饰的引用数据类型变量,它的引用存哪?
这是最后要说清楚的一点。
如果是 static 修饰的引用类型变量,那么它的“引用值”不在栈里,而是在类这一侧,和静态字段一起管理。
它指向的对象,才在堆里。
所以:
- static 引用:类侧
- 引用指向的对象:堆
六、局部变量引用和 static 引用不一样
如果是普通方法里的局部变量引用,比如:
void test() {
A a = new A();
}
这里的 a 这个引用变量是在栈里的。
所以引用要分两种:
- 局部变量引用:栈里
- static 引用:类侧
七、一张总图
方法区 / 元空间
┌────────────────────────────┐
│ 类信息 A │
│ - 类名 A │
│ - 字段 x │
│ - 方法 f │
│ - static 字段 S │
│ - 运行时常量池 │
└────────────────────────────┘
堆
┌────────────────────────────┐
│ A 对象 │
│ - x = 1 │
│ │
│ 字符串对象 "hello" │
└────────────────────────────┘
栈
┌────────────────────────────┐
│ 局部变量 a │
│ a 指向堆里的 A 对象 │
└────────────────────────────┘
八、最后一句话总结
x 跟对象走,S 跟类走;字符串对象在堆里;常量池是目录,字符串常量池是字符串对象的复用池;static 引用属于类侧,不在栈里。