方法区概述(与栈/堆交互)
方法区:类元信息的仓库
方法区(Method Area)是 JVM 规范中定义的线程共享的内存区域,用于存储已被加载的类信息、常量、静态变量、JIT 编译后的代码等。
如果说堆是对象的家,那方法区就是类的家——类的结构、方法的字节码、字段定义,都存在这里。
方法区存什么
┌──────────────────────────────────────────────────────┐
│ 方法区 │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 类型信息 │ │
│ │ - 类的全限定名 │ │
│ │ - 父类的全限定名 │ │
│ │ - 修饰符(public/abstract/final...) │ │
│ │ - 是否是接口 │ │
│ └──────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 字段信息 │ │
│ │ - 字段名、类型、修饰符 │ │
│ │ - 是否是常量(static final) │ │
│ └──────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 方法信息 │ │
│ │ - 方法名、返回类型、参数列表 │ │
│ │ - 修饰符 │ │
│ │ - 方法的字节码 │ │
│ │ - 操作数栈最大深度、局部变量表大小 │ │
│ │ - 异常表 │ │
│ └──────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 运行时常量池 │ │
│ │ - 字符串常量 │ │
│ │ - 数值常量 │ │
│ │ - 类/方法/字段的符号引用 │ │
│ └──────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────┐ │
│ │ JIT 编译后的代码缓存 │ │
│ │ - JIT 编译器生成的本地机器码 │ │
│ └──────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 域引用、方法表、内部结构等 │ │
│ └──────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
方法区与栈、堆的关系
方法区、堆、栈是 JVM 内存模型的三驾马车,它们之间有密切的交互:
方法区 ↔ 堆:对象的类型信息
每个堆中的对象都知道自己是什么类型。这个「知道」是通过对象头中的类型指针实现的:
java
public class HeapObject {
private int value;
public static void main(String[] args) {
HeapObject obj = new HeapObject();
// obj 是堆中的对象
// obj 的类型指针指向方法区中的 HeapObject 类信息
// 通过 obj.getClass() 可以访问到方法区中的类元数据
Class<?> clazz = obj.getClass();
System.out.println(clazz.getName()); // HeapObject
// clazz 指向方法区中该类的 Class 对象
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
方法区 ↔ 栈:方法的执行
栈帧的动态链接指向方法区中的运行时常量池:
java
public class StackMethod {
public int compute() {
// compute() 方法的字节码在方法区中
// 栈帧中的动态链接,通过符号引用访问方法区
return 1 + 2;
}
// 调用时:
// - 方法字节码 → 方法区
// - 局部变量表 → 当前栈帧
// - 操作数栈 → 当前栈帧
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
三者的数据流向
方法区 ──类信息──→ 堆中对象
│ │
│ 动态链接 │ 类型指针
│ │
▼ ▼
栈帧 ──────────→ 操作数栈 + 局部变量表1
2
3
4
5
6
2
3
4
5
6
方法区的演进:PermGen → Metaspace
方法区是 JVM 规范中的一个逻辑概念,但实现方式在 JDK 8 发生了重大变化:
| 维度 | JDK 7 及之前 | JDK 8+ |
|---|---|---|
| 实现 | PermGen(永久代) | Metaspace(元空间) |
| 位置 | 堆内(受 -Xmx 控制) | 本地内存(不受堆大小限制) |
| 大小限制 | 受 -XX:PermSize/-XX:MaxPermSize 控制 | 受 -XX:MaxMetaspaceSize 控制 |
| OOM 原因 | 永久代满(类太多/常量太多) | 元空间满(加载类太多) |
JDK 7:
┌─────────────────────────────────────────┐
│ 堆(Heap) │
│ ┌───────────┐ ┌────────────────┐ │
│ │ 年轻代 │ │ 老年代 │ │
│ └───────────┘ └────────────────┘ │
│ ┌────────────────────────────────────┐ │
│ │ 永久代(PermGen) │ │
│ │ 类信息、常量池、字符串常量 │ │
│ └────────────────────────────────────┘ │
└─────────────────────────────────────────┘
JDK 8+:
┌─────────────────────────────────────────┐
│ 堆(Heap) │
│ ┌───────────┐ ┌────────────────┐ │
│ │ 年轻代 │ │ 老年代 │ │
│ └───────────┘ └────────────────┘ │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 本地内存(Native Memory) │
│ ┌────────────────────────────────────┐ │
│ │ 元空间(Metaspace) │ │
│ │ 类信息、常量池、代码缓存 │ │
│ └────────────────────────────────────┘ │
└─────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
为什么移除 PermGen
永久代(PermGen)有几个根本性问题:
- 大小受堆限制:PermGen 是堆的一部分,设置小了容易 OOM,设置大了影响堆
- 需要完整的 GC:虽然叫「永久」代,但类也会被卸载(需要 Full GC)
- 字符串常量池冲突:JDK 7 把字符串常量池从 PermGen 移到了堆,但空间分配仍然紧张
JDK 8 的元空间方案彻底解决了这些问题。
方法区是线程共享的
方法区是被所有线程共享的内存区域。这意味着:
java
public class MethodAreaSharing {
public static void main(String[] args) {
// Thread A 和 Thread B 共享同一个方法区
// 类信息只加载一份,节省内存
new ThreadA().start();
new ThreadB().start();
}
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
本节小结
方法区的核心要点:
| 关键点 | 说明 |
|---|---|
| 存储内容 | 类信息、字段信息、方法信息、运行时常量池、代码缓存 |
| 线程共享 | 所有线程共用同一方法区 |
| 生命周期 | 与 JVM 进程相同 |
| JDK 7 之前 | PermGen 实现,位于堆内 |
| JDK 8+ | Metaspace 实现,位于本地内存 |
| OOM 原因 | 元空间满(加载类太多) |
方法区与堆、栈共同构成了 JVM 的内存布局,理解它们的交互关系是理解整个 JVM 运行机制的关键。
下一节,我们来看 HotSpot 方法区演进(JDK6/7/8),深入理解方法区的演进历程。
