Skip to content

方法区:JDK 7 PermGen vs JDK 8 Metaspace

你有没有遇到过这种错误:

java.lang.OutOfMemoryError: PermGen space

这通常发生在 Spring MVC 项目启动的时候,或者是大量使用 JSP 的 Web 应用。

这是方法区溢出的经典表现。


一、方法区存储什么?

方法区(Method Area)是 JVM 规范中的一个概念,用于存储:

  • 类的元数据:类的名称、修饰符、父类、实现的接口、字段信息、方法信息
  • 常量:字面量和符号引用
  • 静态变量:static 修饰的变量
  • 即时编译器编译后的代码:JIT 编译后的机器码
  • 运行时常量池:class 文件中的常量池表在运行时的表现形式
┌─────────────────────────────────────────────────────────┐
│                      方法区                            │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  ┌───────────────┐  ┌───────────────┐                  │
│  │   类信息      │  │   常量池       │                  │
│  │  - 类名       │  │  - 字面量      │                  │
│  │  - 父类       │  │  - 符号引用    │                  │
│  │  - 接口       │  │               │                  │
│  │  - 字段       │  └───────────────┘                  │
│  │  - 方法       │  ┌───────────────┐                  │
│  └───────────────┘  │   静态变量     │                  │
│                     └───────────────┘                  │
│                     ┌───────────────┐                  │
│                     │  JIT 编译代码  │                  │
│                     └───────────────┘                  │
│                                                         │
└─────────────────────────────────────────────────────────┘

注意:在 JDK 8 之前,方法区用 PermGen(永久代)实现;JDK 8 之后,用 Metaspace(元空间)实现。


二、JDK 7:PermGen(永久代)

2.1 什么是 PermGen?

PermGen(Permanent Generation,永久代)是 HotSpot 对方法区的实现。

它有以下几个特点:

  • 位置:位于 JVM 堆内存中
  • 大小:通过 -XX:PermSize-XX:MaxPermSize 设置
  • GC:会进行垃圾回收,但回收效率很低
bash
# JDK 7 及之前的方法区配置
-XX:PermSize=64m      # 初始永久代大小
-XX:MaxPermSize=256m  # 最大永久代大小

2.2 PermGen 的问题

PermGen 的设计有很多问题:

问题 1:容易 OOM

字符串常量池(String Pool)在 JDK 7 之前是放在 PermGen 中的。如果应用中有大量字符串,很容易触发 PermGen 溢出。

问题 2:大小难以确定

PermGen 需要预先设置大小,但很难准确估算。设置太小容易 OOM,设置太大浪费内存。

问题 3:GC 复杂且低效

永久代的垃圾回收需要回收类、字符串常量等,但这些对象的回收条件很苛刻,导致永久代的 GC 效率很低。

问题 4:字符串常量池位置不合理

字符串是运行时产生的,放在"永久"的永久代里,显然不合理。


三、JDK 8:Metaspace(元空间)

3.1 什么是 Metaspace?

Metaspace(元空间)是 JDK 8 对方法区的全新实现。

它的设计理念完全不同:

  • 位置:使用本地内存(Native Memory),不在 JVM 堆中
  • 大小:默认不受限制(受物理内存限制)
  • 动态扩展:可以根据需要自动扩展
  • GC:不再需要主动管理类数据,GC 压力大大减小
bash
# JDK 8+ 的元空间配置
-XX:MetaspaceSize=128m    # 初始元空间大小
-XX:MaxMetaspaceSize=256m # 最大元空间大小(默认无限)

3.2 为什么要替换 PermGen?

1. 字符串常量池移出

JDK 7 将字符串常量池移到了堆中,永久代的空间压力大大减小。

2. 类数据太大问题

现在很多框架(如 Spring、Hibernate)会动态生成大量类。PermGen 的固定大小无法满足需求,而 Metaspace 使用本地内存,理论上可以无限扩展。

3. GC 复杂度降低

Metaspace 只存储类的元数据,不再需要复杂的垃圾回收逻辑。当类加载器被回收时,对应的元数据也会被回收。

4. 简化 JVM 实现

把方法区移出堆,可以让 JVM 的内存管理更加清晰。

JDK 8 内存布局
┌─────────────────────────────────────────────────────────────────┐
│  JVM 堆(Heap)                                                   │
│  - 对象实例                                                       │
│  - 数组                                                          │
│  - 字符串常量池(JDK 8+)                                         │
│  - Young Generation / Old Generation                             │
├─────────────────────────────────────────────────────────────────┤
│  本地内存(Native Memory)                                        │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                      Metaspace(元空间)                   │    │
│  │  - 类元数据                                               │    │
│  │  - 方法信息                                               │    │
│  │  - 静态变量(JDK 7 及之前在 PermGen)                        │    │
│  │  - 即时编译器代码                                          │    │
│  │  - 运行时常量池                                            │    │
│  └─────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘

四、版本差异对比

特性PermGen(JDK 7 及之前)Metaspace(JDK 8+)
位置JVM 堆内存本地内存
大小限制固定(-XX:PermSize)动态(受物理内存限制)
字符串常量池在 PermGen 中在堆中(JDK 7 移入)
默认大小固定动态计算
溢出表现OutOfMemoryError: PermGen spaceOutOfMemoryError: Metaspace
GC低效,需主动管理类加载器回收时自动清理

五、Metaspace 溢出场景

即使 Metaspace 使用本地内存,也会溢出:

java
// 大量生成动态类,导致 Metaspace 溢出
public class MetaspaceOOM {
    public static void main(String[] args) {
        while (true) {
            // 使用 CGLIB 动态生成类
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(MetaspaceOOM.class);
            enhancer.setCallbackFilter(new CallbackFilter() {
                @Override
                public int accept(Method method) {
                    return 0;
                }
            });
            enhancer.create();
        }
    }
}

常见触发场景:

  • Spring/Hibernate 动态代理:CGLIB 生成大量代理类
  • JSP 编译:每个 JSP 页面编译后会生成一个类
  • OSGi 框架:动态加载卸载 bundle
  • 自定义 ClassLoader:频繁创建新的类加载器

六、常用配置参数

bash
# JDK 8+ 元空间配置
-XX:MetaspaceSize=128m     # 初始元空间大小(触发 Full GC 的阈值)
-XX:MaxMetaspaceSize=256m # 最大元空间大小(默认无限)

# 调试参数
-XX:+TraceClassLoading     # 追踪类加载
-XX:+TraceClassUnloading   # 追踪类卸载

注意MetaspaceSize 不仅是初始大小,也是触发 Metaspace Full GC 的阈值。当 Metaspace 使用超过这个值时,会触发 Full GC。


七、面试高频问题

问题 1:JDK 8 为什么废弃了永久代?

主要原因是永久代容易 OOM、大小难以确定、GC 复杂。元空间使用本地内存,理论上可以无限扩展,解决了这些问题。

问题 2:方法区和堆有什么区别?

堆存储对象实例,方法区存储类信息。堆是所有线程共享的,GC 会频繁发生;方法区存储的是"元数据",GC 频率相对较低。

问题 3:Metaspace 会 GC 吗?

会。当类加载器被回收时,对应的类元数据会被清理。但相比 PermGen,Metaspace 的 GC 简单得多——只要类加载器不存在了,它的元数据就可以回收。

问题 4:静态变量在 JDK 8 中存在哪?

JDK 8 把静态变量和实例变量放在一起了,都存在堆中。方法区只存储类的元数据(类的结构信息),不再存储静态变量的值。


留给你的问题

我们讲了方法区的演进:从 JDK 7 的 PermGen 到 JDK 8 的 Metaspace。

你有没有想过:既然 Metaspace 使用本地内存,理论上可以无限扩展,那为什么还要设置 -XX:MaxMetaspaceSize

实际上,不限制 Metaspace 大小可能导致系统内存被耗尽。如果不断有新的类加载进来(比如动态代理、热部署),Metaspace 会一直扩展,直到系统可用内存耗尽。

所以,即使 Metaspace 不在堆中,也需要通过 -XX:MaxMetaspaceSize 限制它的大小,防止影响其他进程。

另外,-XX:MetaspaceSize 的值也是一个重要的调优点——如果设置得太小,会频繁触发 Full GC。

基于 VitePress 构建