方法区与元空间区别

候选人小马在面试蚂蚁 P7 时,面试官看了一眼简历上"JDK 8 升级经验",问道:

"JDK 8 为什么移除 PermGen?Metaspace 是什么?"

小马说:"PermGen 空间固定,容易 OOM..."面试官追问:"Metaspace 使用的是哪块内存?默认大小是多少?"

小马答不上来。面试官继续追问:"字符串常量池从 PermGen 移到堆中,具体怎么移的?"

小马彻底卡住了...

一、核心问题:方法区与 Metaspace 🔴

1.1 问题拆解

第一层:历史背景(为什么要改?)
  "JDK 8 为什么移除 PermGen?它有什么问题?"
  考察点:固定大小、字符串常量池竞争、Full GC 负担

第二层:Metaspace 设计(怎么实现的?)
  "Metaspace 使用的是什么内存?默认大小是多少?"
  考察点:本地内存、不受 GC 直接管理

第三层:数据迁移(移了什么?)
  "JDK 8 中哪些内容从 PermGen 移到了 Metaspace?哪些移到了堆?"
  考察点:字符串常量池、静态变量

1.2 ❌ 错误示范

候选人原话 A:"Metaspace 在堆里面。"

问题诊断:Metaspace 使用的是本地内存(Native Memory),不在 Java 堆中。这意味着 Metaspace 的增长不会直接导致堆 OOM,但可能导致整个进程内存不足。

候选人原话 B:"JDK 8 之后就没有方法区了。"

问题诊断:方法区是 JVM 规范中的概念,Metaspace 是 HotSpot 对方法区的实现。JDK 8+ 方法区仍然存在,只是实现方式变了。

1.3 标准回答

P5 级别:PermGen 的问题

JDK 7 PermGen 的三大问题

  1. 固定大小,难以调优:PermGen 大小在 JVM 启动时确定(-XX:MaxPermSize),难以根据应用需求调整

  2. 字符串常量池占用 PermGen 空间:大量使用 String.intern() 的应用会耗尽 PermGen

  3. Full GC 时收集 PermGen:增加了 Full GC 的停顿时间

// JDK 7 中,字符串常量池在 PermGen
String s1 = new String("hello");  // 对象在堆
String s2 = "hello";              // 常量在 PermGen(运行时常量池)

// 大量调用 intern() → PermGen OOM
for (int i = 0; i < 10000000; i++) {
    s.intern();  // JDK 7 中会耗尽 PermGen
}

P6 级别:Metaspace 的设计

Metaspace 的架构

graph TD
    OS[操作系统内存] --> Meta[Metaspace 元空间]
    Meta --> CM[类元数据<br/>Klass 结构]
    Meta --> CP[运行时常量池<br/>符号引用]
    Meta --> Code[方法字节码<br/>JIT 编译代码]
    Meta --> DT[动态生成的类<br/>CGlib/Javaassist]

    Heap[Java 堆] --> SCP[字符串常量池<br/>JDK 8]

    Meta -.->|自动调整| GC[JVM GC<br/>只回收类加载器]

Metaspace 的关键特性

特性说明
内存来源使用本地内存(Native Memory),不占用堆
默认大小无固定上限(受物理内存限制),可设置 -XX:MaxMetaspaceSize
GC 策略类加载器及其加载的类在没有引用时,可以被 GC 卸载
自动调整MetaspaceSize 是触发 GC 的阈值,可自动增长

关键参数

# JDK 8 Metaspace 配置
-XX:MetaspaceSize=128m    # 初始阈值(触发 GC 的阈值)
-XX:MaxMetaspaceSize=256m # 最大限制(默认无限制)

# MetaspaceSize 动态调整机制:
# 初始 = 估算值(根据平台和类数量)
# 达到阈值后触发 Metaspace GC
# 如果 GC 后空间释放,继续增长
# 如果 GC 后空间持续紧张,限制增长

P7 级别:数据迁移详解

JDK 8 的数据迁移

内容JDK 7 位置JDK 8 位置迁移原因
类元数据(Klass)PermGenMetaspace(本地内存)不再受堆大小限制
运行时常量池PermGenMetaspace(本地内存)与类元数据一起更合理
JIT 编译代码PermGen(CodeCache)本地内存(CodeCache)同上
字符串常量池PermGenJava 堆避免与类元数据竞争空间
静态变量PermGenMetaspace(引用)+ 堆(对象)引用在 Metaspace,值在堆

字符串常量池迁移的具体实现

JDK 7 将字符串常量池从 PermGen 移到了 Java 堆中:

// JDK 7 之前:字符串常量池在 PermGen
// JDK 7 及之后:字符串常量池在堆中

String s1 = "hello";           // "hello" 在堆中的字符串常量池
String s2 = new String("hello"); // s2 对象在堆,字符串内容在常量池

// String.intern() 的行为变化
// JDK 7+:intern 字符串如果常量池中没有,
//         将该字符串对象的引用添加到常量池
//         (之前是在 PermGen 创建新字符串)

静态变量的迁移

public class StaticClass {
    static Object obj = new Object();    // 引用(指针)在 Metaspace
                                            // Object 实例在堆
    static String str = "hello";          // 字符串在堆中的字符串常量池
    static int num = 42;                   // 基本类型在 Metaspace
    static final int CONST = 100;         // 常量在 Metaspace(编译期优化)
}

【面试官心理】 这道题我能问到 P7 级别,是因为 JDK 8 的方法区变更是 Java 最重要的版本变更之一,涉及了内存管理、GC 策略、字符串池等多个方面的综合理解。能说出静态变量迁移细节的候选人说明他对 JDK 演进有深入研究。

1.4 追问升级

追问 1:Metaspace 也会 OOM 吗?

会。OutOfMemoryError: Metaspace。典型原因:

  • 大量动态类生成(CGlib 代理、模板引擎、JSP 编译)
  • 元空间上限设置过小(-XX:MaxMetaspaceSize
  • 类加载器泄漏(大量自定义类加载器未卸载)

追问 2:为什么 Metaspace 不使用 GC 直接管理?

Metaspace 的 GC 是类加载器卸载(Class Loader unloading)。当一个类加载器及其加载的所有类都不再被引用时,它们可以被卸载,释放 Metaspace。这个 GC 与 Java 堆的 GC 是独立的。

二、生产避坑 🟡

2.1 Metaspace OOM 的常见场景

// 场景 1:大量 CGlib 动态生成代理类
// Spring 使用 CGlib 生成 AOP 代理
// 如果 AOP 使用不当,每次请求生成新代理类 → Metaspace OOM

// 场景 2:JSP 频繁编译
// 每个 JSP 页面首次访问都会编译成类 → 大量 JSP → 类爆炸

// 场景 3:OSGi 动态模块化
// OSGi 允许动态安装/卸载 bundle
// bundle 卸载不当 → 类加载器泄漏

2.2 Metaspace 监控

# jstat 监控 Metaspace
jstat -gc <pid> | grep MC    # Metaspace 容量 (KB)
jstat -gc <pid> | grep MU    # Metaspace 使用量 (KB)
jstat -gcutil <pid> 1000    # 查看 GC 统计(包括 Metaspace)

# jcmd 查看 Metaspace 详情
jcmd <pid> VM.native_memory summary

三、方法区参数配置 🟢

3.1 JDK 8 Metaspace 参数

# 推荐配置
-XX:MetaspaceSize=256m   # 初始阈值
-XX:MaxMetaspaceSize=512m # 最大限制(生产环境必须设置)

# 为什么不设置 MaxMetaspaceSize?
# 因为 Metaspace 增长受物理内存限制
# 如果不设上限,可能导致进程 OOM(耗尽 swap)

3.2 JDK 7 PermGen 参数(已废弃)

# JDK 7 的 PermGen 配置(仅用于历史参考)
-XX:PermSize=256m        # 初始大小
-XX:MaxPermSize=512m     # 最大大小
💡

面试加分点:能说出"JDK 11 之后,JVM 引入了 Application Class Data Sharing(AppCDS),允许共享类元数据的压缩数据,减少启动时间和 Metaspace 占用",说明他关注了 JDK 9+ 的新特性。

⚠️

面试陷阱:被问到"Metaspace 的 GC 和堆的 GC 是同一个 GC 吗",很多人会说"是"。准确答案是:不是同一个。Metaspace GC(类卸载)和 Java 堆 GC(对象回收)是独立的两个 GC 过程,由不同的 GC 线程负责。