方法区: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:会进行垃圾回收,但回收效率很低
# 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 压力大大减小
# 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 space | OutOfMemoryError: Metaspace |
| GC | 低效,需主动管理 | 类加载器回收时自动清理 |
五、Metaspace 溢出场景
即使 Metaspace 使用本地内存,也会溢出:
// 大量生成动态类,导致 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:频繁创建新的类加载器
六、常用配置参数
# 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。
