03.5 - 方法区与元空间
定位: 存储类元数据、运行时常量池、JIT 编译代码的线程共享区域——JDK 8 从 PermGen 迁移到 Metaspace 面试高频度: ⭐⭐⭐⭐ 考查方式: PermGen 到 Metaspace 的迁移原因、类元数据存储内容、Metaspace OOM
一、这是什么?为什么需要它?
方法区(Method Area)是 JVM 规范定义的一个逻辑概念——所有线程共享,存储已加载的类信息、常量、静态变量、JIT 编译后的代码等数据。
如果说堆是对象的"家"(对象实例存储的地方),那方法区就是对象的"设计图仓库"——每个对象的类型信息(类名、父类、接口、方法、字段定义等)都存储在这里。
堆上的对象: 方法区的类元数据:
┌────────────────────┐ ┌──────────────────────┐
│ new String("hello")│ 指向类型 │ String 类的元数据 │
│ obj │ ─────────→ │ ├ 类名: java/lang/String│
│ │ │ ├ 父类: Object │
│ │ │ ├ 接口: Serializable │
│ │ │ ├ 方法表 │
│ │ │ │ ├ length() → 地址 │
│ │ │ │ ├ charAt() → 地址 │
│ │ │ │ └ ... │
│ │ │ └ 字段表 │
└────────────────────┘ └──────────────────────┘为什么需要单独存储类元数据? JVM 执行期间需要频繁查询类型信息:instanceof 检查、方法调用分派、强转类型校验、反射 API。如果每次都需要从 Class 文件重新解析,性能根本无法接受。方法区就是这些运行时类型信息的缓存,类加载一次,后续整个生命周期复用。
二、原理拆解
2.1 方法区存储什么?
┌──────────────────────────────────────────────┐
│ 方法区 / Metaspace │
│ │
│ ┌─ 类元数据 (Class Metadata) ─────────────┐ │
│ │ 每个加载的类对应一组元数据: │ │
│ │ - 类名、父类、接口列表 │ │
│ │ - 字段信息 (名称、类型、修饰符) │ │
│ │ - 方法信息 (名称、描述符、字节码) │ │
│ │ - 方法表 (vtable / itable) │ │
│ │ - 常量池引用 │ │
│ │ - 注解信息 │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ┌─ 运行时常量池 (Runtime Constant Pool) ───┐ │
│ │ Class 文件常量池在类加载后: │ │
│ │ - 符号引用 (待解析的类/方法/字段引用) │ │
│ │ - 直接引用 (已解析的内存地址) │ │
│ │ - String.intern() 动态加入的字符串 (JDK7前)│
│ └─────────────────────────────────────────┘ │
│ │
│ ┌─ JIT 编译代码缓存 ──────────────────────┐ │
│ │ JIT 编译的热点方法机器码 │ │
│ │ (由 -XX:ReservedCodeCacheSize 控制) │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ┌─ 静态变量 (JDK 7 后) ───────────────────┐ │
│ │ 类变量 (static field) 的引用或值 │ │
│ │ (HotSpot 实际实现在堆上, 逻辑上在方法区) │ │
│ └─────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘2.2 PermGen 时代:为什么 OOM 像流行病?
在 JDK 7 及之前,方法区的实现被称为永久代(PermGen)——因为它和堆连着,被认为是"永久"存在的数据(GC 不轻易回收)。
PermGen 的两个致命问题:
┌─ 问题 1: 固定大小 ─────────────────────────┐
│ -XX:MaxPermSize 控制上限 (默认 64MB/82MB) │
│ 调小 → OOM, 调大 → 浪费 │
│ 生产环境难以预测需要多大 PermGen │
└─────────────────────────────────────────────┘
┌─ 问题 2: 字符串常量池在 PermGen ───────────┐
│ String.intern() 动态增长, PermGen 满了 OOM │
│ PermGen GC 条件苛刻, intern 的字符串不回收 │
│ 生产环境: 同一个字符串被 intern 后永不释放 │
└─────────────────────────────────────────────┘典型场景: 在 Tomcat 中频繁热部署应用。每次部署用新的 ClassLoader 加载类,但上一个 ClassLoader 如果还存在引用,PermGen 中的类元数据无法被卸载。反复几次后 PermGen 满 → java.lang.OutOfMemoryError: PermGen space。
解决方案(当时): 调大 -XX:MaxPermSize=256m,或者干脆不要频繁热部署。但这些都是治标不治本。
2.3 JDK 8 迁移到 Metaspace
JDK 8 彻底移除了 PermGen,用元空间(Metaspace) 替代,存储在本地内存(Native Memory) 中,不再属于 JVM 堆。
JDK 8 之前的 JVM 内存结构 JDK 8 之后的 JVM 内存结构
(方法区在 PermGen, 属于堆) (方法区在 Metaspace, 在本地内存)
┌─────────────────────┐ ┌─────────────────────┐
│ 堆 │ │ 堆 │
│ ┌───────┬────────┐ │ │ ┌───────┬────────┐ │
│ │ Young │ Old │ │ │ │ Young │ Old │ │
│ ├───────┴────────┤ │ │ ├───────┴────────┤ │
│ │ PermGen │ │ │ │ 字符串常量池 │ │
│ │ ├ 类元数据 │ │ │ └────────────────┘ │
│ │ ├ 运行时常量池 │ │ └─────────────────────┘
│ │ ├ 字符串常量池 │ │ ┌─────────────────────┐
│ │ └ JIT缓存 │ │ │ 本地内存 (Native) │
│ └─────────────────┘ │ │ ┌─ Metaspace ─────┐│
│ │ │ │ ├ 类元数据 ││
│ 受 -Xmx 控制 │ │ │ ├ 运行时常量池 ││
│ -XX:MaxPermSize 单独│ │ │ └ JIT 缓存 ││
│ │ │ └─────────────────┘│
└─────────────────────┘ │ 不受 -Xmx 控制 │
│ -XX:MaxMetaspaceSize │
└─────────────────────┘三个关键变更:
| 变更 | 效果 |
|---|---|
| 类元数据移到本地内存 | 默认无上限(受 OS 物理内存限制),不再有 PermGen OOM |
| 字符串常量池移到堆 | 堆 GC 自动回收不再被引用的 intern 字符串 |
| 运行时常量池仍在 Metaspace | 因为它与类元数据紧密关联,类卸载时随类一起释放 |
新的 JVM 参数:
# Metaspace 初始大小 (默认约 21MB, 触发 GC 的阈值)
-XX:MetaspaceSize=256m
# Metaspace 最大大小 (默认无限, 受 OS 内存限制)
-XX:MaxMetaspaceSize=512m
# 每次 GC 后 Metaspace 扩容的增量
-XX:MinMetaspaceFreeRatio=40 # GC 后空闲 Metaspace 比例下限
-XX:MaxMetaspaceFreeRatio=70 # GC 后空闲 Metaspace 比例上限注意:
MetaspaceSize不是初始分配大小,而是触发第一次 Metaspace GC 的阈值。如果设为 256m,Metaspace 使用量达到 256m 时会触发一次 GC 来卸载类。这是为了尽早发现类加载泄漏。
为什么 JDK 8 能做这个迁移? 移除了 PermGen 后,类元数据和 Java 对象在内存管理上彻底解耦:
- 堆 GC 只需关心对象,不必遍历 PermGen 的引用
- Metaspace 的扩容/收缩由 OS 管理,JVM 不需要在堆内预留空间
- 类卸载条件不变但更容易满足——Metaspace 空间不足时自动触发类卸载
三、图解全景
┌─────────────────────────────────────────────────────────────────┐
│ PermGen → Metaspace 迁移全景对比 │
│ │
│ JDK 7 (PermGen) JDK 8+ (Metaspace) │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ 堆 (受 -Xmx 控制) │ │ 堆 (受 -Xmx 控制) │ │
│ │ ┌─────┬────┬──────┐ │ │ ┌─────┬────┬──────┐ │ │
│ │ │Eden │ S │ Old │ │ │ │Eden │ S │ Old │ │ │
│ │ ├─────┴────┴──────┤ │ │ ├─────┴────┴──────┤ │ │
│ │ │ 字符串常量池 │ │ │ │ 字符串常量池 │ │ │
│ │ └─────────────────┘ │ │ └─────────────────┘ │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ PermGen (永久代) │ │ Metaspace (本地内存) │ │
│ │ 固定大小 │ │ 默认无上限 │ │
│ │ -XX:MaxPermSize │ │ -XX:MaxMetaspaceSize│ │
│ │ │ │ │ │
│ │ ├ 类元数据 │ │ ├ 类元数据 │ │
│ │ ├ 运行时常量池 │ │ ├ 运行时常量池 │ │
│ │ ├ 字符串常量池 ←OOM │ │ ├ JIT 缓存 │ │
│ │ └ JIT 缓存 │ │ └ (无字符串常量池) │ │
│ │ │ │ │ │
│ │ 堆 GC 必须扫描 │ │ Metaspace 扩容/收缩 │ │
│ │ PermGen 引用 │ │ 由 OS 管理 │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
│ 解决问题的核心: │
│ 1. 字符串常量池归堆 → GC 自动回收 │
│ 2. 类元数据归本地内存 → 默认无上限 │
│ 3. 两个区域独立管理 → 堆 GC 不再扫描类元数据 │
└─────────────────────────────────────────────────────────────────┘四、实战验证
验证 1: Metaspace OOM(故意用不同 ClassLoader 加载类)
import java.net.URL;
import java.net.URLClassLoader;
public class MetaspaceOOMDemo {
public static void main(String[] args) throws Exception {
// 不断用新的 ClassLoader 加载类, 耗尽 Metaspace
while (true) {
URLClassLoader loader = new URLClassLoader(
new URL[]{new URL("file:/tmp/classes/")}
);
Class<?> clazz = loader.loadClass("com.example.Dummy");
// 不关闭 loader → 类无法卸载 → Metaspace 持续增长
}
}
}# 设置较小的 Metaspace 上限, 加速 OOM
java -XX:MaxMetaspaceSize=64m MetaspaceOOMDemo
# 很快会输出: java.lang.OutOfMemoryError: Metaspace如果将 -XX:MaxMetaspaceSize 设为 0 或 不设(默认无限),程序会耗尽物理内存,造成操作系统级别的 OOM Killer。
验证 2: 查看 Metaspace 使用量
# 启动 Java 程序后, 用 jstat 查看 Metaspace 使用情况
jstat -gcmetacapacity <pid>
# 输出示例:
# MCMN MCMX MC CCSMN CCSMX CCSC YGC FGC
# 0.0 1056768.0 4480.0 0.0 1048576.0 512.0 0 0
# ↑ Metaspace 最小/最大/当前使用量 (KB)# 或使用 jcmd
jcmd <pid> VM.metadataMetaspace 的最大值(MCMX)默认约为 1GB(1056768 KB),这不是限制,而是预留的虚拟地址空间。
五、面试视角
| 追问 | 答案要点 |
|---|---|
| 为什么 JDK 8 把永久代换成元空间? | PermGen 固定大小难以调优,String.intern() 动态增长导致 OOM。Metaspace 使用本地内存默认无上限,字符串常量池移入堆由 GC 自动回收 |
| Metaspace 会出现 OOM 吗? | 会。如果用 -XX:MaxMetaspaceSize 设置了上限,达到上限会 OOM。不设上限时,耗尽 OS 物理内存也会 OOM(或触发 OS OOM Killer)。典型场景:动态类加载(CGLIB、热部署) |
| PermGen 和 Metaspace 的区别? | PermGen 在堆内固定大小,Metaspace 在本地内存默认无上限;PermGen 包含字符串常量池(易 OOM),Metaspace 不含(字符串池已移入堆);PermGen 需要堆 GC 扫描,Metaspace 独立管理 |
| 方法区存储什么? | 类元数据(类名、字段、方法、vtable/itable)、运行时常量池、JIT 编译代码缓存、静态变量引用 |
| 永久代移除后,运行时常量池去哪了? | Metaspace。因为运行时常量池与类元数据生命周期一致,类卸载时随类一起释放 |
| -XX:MetaspaceSize 是什么? | 触发第一次 Metaspace GC 的阈值,不是初始分配大小。设为较大值可以推迟 Metaspace GC,避免频繁类卸载 |
| 什么场景下类可以被卸载? | ClassLoader 不可达 + 该 ClassLoader 加载的所有类不再被使用。只有自定义 ClassLoader 加载的类可能被卸载(Bootstrap/Ext/App 加载的类永不解散) |
📚 相关链接
- **类加载生命周期** — 类加载在方法区构建类元数据的完整过程
- **Java堆** — 方法区与堆的交互(对象指向类元数据)
- **运行时常量池** — 运行时常量池是方法区的重要组成部分
- ← 返回 **运行时数据区索引**