Skip to content

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)           │  │
│  │  └─ 符号引用                              │  │
│  └────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────┘

PermGen 的典型问题

PermGen 大小默认只有几十 MB,在大型应用或使用反射、动态代理、CGLib 的场景下很容易耗尽:

java
// 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
        }
    }
}
java.lang.OutOfMemoryError: PermGen space

JDK 6 相关参数

bash
# 设置 PermGen 大小(JDK 6)
java -XX:PermSize=128m -XX:MaxPermSize=256m MyApp

JDK 7:过渡期——字符串常量池出堆

JDK 7 是一个过渡版本,最大的变化是把字符串常量池从 PermGen 移到了堆中。

JDK 7 堆结构:
┌──────────────────────────────────────────────────┐
│                      堆(Heap)                     │
│                                                    │
│  ┌──────────────────┐  ┌──────────────────────┐ │
│  │       年轻代       │  │        老年代         │ │
│  │  Eden │ S0 │ S1 │  │                       │ │
│  └──────────────────┘  └──────────────────────┘ │
│  ┌────────────────────────────────────────────┐  │
│  │           字符串常量池(StringTable)       │  │
│  │           (JDK 7 从 PermGen 移入堆)      │  │
│  └────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────┐  │
│  │        PermGen(缩小版)                    │  │
│  │  ├─ 类信息(类结构、字段、方法字节码)      │  │
│  │  ├─ 运行时常量池(非字符串部分)          │  │
│  │  └─ 符号引用                              │  │
│  └────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────┘

为什么要移动字符串常量池

PermGen 太小,字符串常量池又经常占用大量空间。把字符串常量池移到堆中,可以利用堆的 GC 机制,更好地管理字符串对象。

java
// 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(字符串常量池共享)
    }
}

JDK 8:Metaspace 取代 PermGen

JDK 8 彻底移除了 PermGen,用 Metaspace(元空间) 取而代之。

JDK 8 堆 + 本地内存结构:
┌──────────────────────────────────────────────────┐
│                      堆(Heap)                     │
│  ┌──────────────────┐  ┌──────────────────────┐ │
│  │       年轻代       │  │        老年代         │ │
│  │  Eden │ S0 │ S1 │  │                       │ │
│  └──────────────────┘  └──────────────────────┘ │
└──────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────┐
│               本地内存(Native Memory)               │
│  ┌────────────────────────────────────────────┐  │
│  │              Metaspace(元空间)              │  │
│  │  ├─ 类信息(类结构、字段、方法字节码)        │  │
│  │  ├─ 运行时常量池                            │  │
│  │  ├─ 字符串常量(interned strings,JDK 8)   │  │
│  │  └─ 代码缓存(JIT 编译后的机器码)          │  │
│  └────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────┘

Metaspace 的优势

维度PermGenMetaspace
位置堆内(受 -Xmx 控制)本地内存(不受堆限制)
默认大小固定(几十 MB)自动增长(直到 MaxMetaspaceSize)
溢出处理OOM: PermGen spaceOOM: Metaspace space
GC需要显式 Full GC 才回收多数情况下自动回收
OOM 风险容易(默认太小)低(可自动扩展)

JDK 8 典型 OOM

虽然 Metaspace 可以自动扩展,但如果设置了上限(-XX:MaxMetaspaceSize),仍然可能 OOM:

java
// JDK 8 中,如果元空间设置上限且加载类太多
// java -XX:MaxMetaspaceSize=128m
// ...
// java.lang.OutOfMemoryError: Metaspace

参数对照表

功能JDK 6JDK 7JDK 8
类信息区PermGenPermGenMetaspace
字符串常量池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、热加载框架等),可能需要调高上限。

bash
# JDK 8 推荐的 Metaspace 配置
java -XX:MetaspaceSize=256m \    # 初始大小
     -XX:MaxMetaspaceSize=512m \ # 最大大小
     MyApp

本节小结

HotSpot 方法区的演进:

版本实现关键变化
JDK 6PermGen(堆内)字符串常量池在 PermGen 中,默认 ~21MB
JDK 7PermGen(缩小)字符串常量池移入堆,PermGen 缩小
JDK 8Metaspace(本地内存)移除 PermGen,类信息进入本地内存,可自动扩展

理解演进历史,有助于理解为什么某些旧代码的参数设置(-XX:PermSize)在 JDK 8+ 不再有效,以及为什么升级 JDK 后某些 OOM 问题消失了。

下一节,我们来看 方法区大小设置与 OOM 案例

基于 VitePress 构建