HotSpot 方法区演进(JDK6/7/8)
方法区的三个时代
HotSpot 实现方法区的历史,可以分为三个阶段:JDK 6、JDK 7、JDK 8。这三个版本的方法区实现有显著差异,理解这些差异有助于理解为什么有些代码在升级 JDK 后行为不同。
JDK 6:经典 PermGen
JDK 6 的方法区使用 PermGen(Permanent Generation) 实现,位于堆内。
JDK 6 堆结构:
┌──────────────────────────────────────────────────┐
│ 堆(Heap) │
│ │
│ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ 年轻代 │ │ 老年代 │ │
│ │ Eden │ S0 │ S1 │ │ │ │
│ └──────────────────┘ └──────────────────────┘ │
│ ┌────────────────────────────────────────────┐ │
│ │ PermGen(永久代) │ │
│ │ ├─ 类信息(类的结构、字段、方法字节码) │ │
│ │ ├─ 运行时常量池 │ │
│ │ ├─ 字符串常量池(StringTable) │ │
│ │ └─ 符号引用 │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PermGen 的典型问题
PermGen 大小默认只有几十 MB,在大型应用或使用反射、动态代理、CGLib 的场景下很容易耗尽:
// JDK 6 中,以下场景容易触发 PermGen OOM
public class PermGenOOM {
public static void main(String[] args) {
// 动态生成大量类
// Spring、Hibernate 等框架大量使用反射和动态代理
// 每个 Bean 类、代理类都会占用 PermGen
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Hello.class);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method m, MethodProxy proxy, Object[] args) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create(); // 每次创建新的代理类,占用 PermGen
}
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java.lang.OutOfMemoryError: PermGen spaceJDK 6 相关参数
# 设置 PermGen 大小(JDK 6)
java -XX:PermSize=128m -XX:MaxPermSize=256m MyApp2
JDK 7:过渡期——字符串常量池出堆
JDK 7 是一个过渡版本,最大的变化是把字符串常量池从 PermGen 移到了堆中。
JDK 7 堆结构:
┌──────────────────────────────────────────────────┐
│ 堆(Heap) │
│ │
│ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ 年轻代 │ │ 老年代 │ │
│ │ Eden │ S0 │ S1 │ │ │ │
│ └──────────────────┘ └──────────────────────┘ │
│ ┌────────────────────────────────────────────┐ │
│ │ 字符串常量池(StringTable) │ │
│ │ (JDK 7 从 PermGen 移入堆) │ │
│ └────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────┐ │
│ │ PermGen(缩小版) │ │
│ │ ├─ 类信息(类结构、字段、方法字节码) │ │
│ │ ├─ 运行时常量池(非字符串部分) │ │
│ │ └─ 符号引用 │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
为什么要移动字符串常量池
PermGen 太小,字符串常量池又经常占用大量空间。把字符串常量池移到堆中,可以利用堆的 GC 机制,更好地管理字符串对象。
// JDK 7 中:
// "hello" 的字符串对象存储在堆中,受 -Xmx 控制
// 字符串常量池(StringTable)本身也在堆中
public class StringInHeap {
public static void main(String[] args) {
String s1 = "hello"; // s1 指向堆中的字符串对象
String s2 = "hello"; // s2 指向同一个字符串对象
System.out.println(s1 == s2); // true(字符串常量池共享)
}
}2
3
4
5
6
7
8
9
10
JDK 8:Metaspace 取代 PermGen
JDK 8 彻底移除了 PermGen,用 Metaspace(元空间) 取而代之。
JDK 8 堆 + 本地内存结构:
┌──────────────────────────────────────────────────┐
│ 堆(Heap) │
│ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ 年轻代 │ │ 老年代 │ │
│ │ Eden │ S0 │ S1 │ │ │ │
│ └──────────────────┘ └──────────────────────┘ │
└──────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────┐
│ 本地内存(Native Memory) │
│ ┌────────────────────────────────────────────┐ │
│ │ Metaspace(元空间) │ │
│ │ ├─ 类信息(类结构、字段、方法字节码) │ │
│ │ ├─ 运行时常量池 │ │
│ │ ├─ 字符串常量(interned strings,JDK 8) │ │
│ │ └─ 代码缓存(JIT 编译后的机器码) │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Metaspace 的优势
| 维度 | PermGen | Metaspace |
|---|---|---|
| 位置 | 堆内(受 -Xmx 控制) | 本地内存(不受堆限制) |
| 默认大小 | 固定(几十 MB) | 自动增长(直到 MaxMetaspaceSize) |
| 溢出处理 | OOM: PermGen space | OOM: Metaspace space |
| GC | 需要显式 Full GC 才回收 | 多数情况下自动回收 |
| OOM 风险 | 容易(默认太小) | 低(可自动扩展) |
JDK 8 典型 OOM
虽然 Metaspace 可以自动扩展,但如果设置了上限(-XX:MaxMetaspaceSize),仍然可能 OOM:
// JDK 8 中,如果元空间设置上限且加载类太多
// java -XX:MaxMetaspaceSize=128m
// ...
// java.lang.OutOfMemoryError: Metaspace2
3
4
参数对照表
| 功能 | JDK 6 | JDK 7 | JDK 8 |
|---|---|---|---|
| 类信息区 | PermGen | PermGen | Metaspace |
| 字符串常量池 | PermGen | 堆 | 堆 |
| 初始大小参数 | -XX:PermSize | -XX:PermSize | -XX:MetaspaceSize |
| 最大大小参数 | -XX:MaxPermSize | -XX:MaxPermSize | -XX:MaxMetaspaceSize |
| 默认大小 | ~21MB | ~21MB | 约 21MB(由 MetaspaceSize 控制) |
升级 JDK 时的注意事项
1. 反射/动态代理场景
JDK 8 之前 PermGen 太小(默认几十 MB),JDK 8+ 的 Metaspace 可以自动扩展,这类问题基本消失。
2. 字符串常量行为
JDK 7 把字符串常量池移到了堆中,意味着 interned 字符串和普通字符串都受相同 GC 影响。大型应用大量使用 String.intern() 时,需要注意对 GC 的影响。
3. 元空间 OOM
如果使用 -XX:MaxMetaspaceSize 设置了上限,需要确保值足够。如果加载了极多的类(动态类生成、OSGi、热加载框架等),可能需要调高上限。
# JDK 8 推荐的 Metaspace 配置
java -XX:MetaspaceSize=256m \ # 初始大小
-XX:MaxMetaspaceSize=512m \ # 最大大小
MyApp2
3
4
本节小结
HotSpot 方法区的演进:
| 版本 | 实现 | 关键变化 |
|---|---|---|
| JDK 6 | PermGen(堆内) | 字符串常量池在 PermGen 中,默认 ~21MB |
| JDK 7 | PermGen(缩小) | 字符串常量池移入堆,PermGen 缩小 |
| JDK 8 | Metaspace(本地内存) | 移除 PermGen,类信息进入本地内存,可自动扩展 |
理解演进历史,有助于理解为什么某些旧代码的参数设置(-XX:PermSize)在 JDK 8+ 不再有效,以及为什么升级 JDK 后某些 OOM 问题消失了。
下一节,我们来看 方法区大小设置与 OOM 案例。
