为什么用元空间(Metaspace)替代永久代(PermGen)?元空间会 OOM 吗?
简化版
永久代(PermGen)是 JDK 7 及以前存放类元信息(类的结构、方法、常量池等)的区域,它属于堆的一部分、大小固定(-XX:MaxPermSize),最大的问题是容易 OOM:大小难以预估,动态生成类多(如反射、CGLIB、JSP)时经常撑爆 OutOfMemoryError: PermGen space。JDK 8 用元空间(Metaspace) 替代它,最大变化是元空间用本地内存(堆外),默认大小只受物理内存限制,不再固定,大幅降低了 OOM 概率。但元空间仍会 OOM——如果一直动态生成类(尤其是类加载器泄漏),本地内存也会被耗尽,报 OutOfMemoryError: Metaspace。
详细版
永久代 vs 元空间:
| 维度 | 永久代(PermGen,≤JDK 7) | 元空间(Metaspace,JDK 8+) |
|---|---|---|
| 位置 | JVM 堆内 | 本地内存(堆外) |
| 大小 | 固定,-XX:MaxPermSize | 默认只受物理内存限制 |
| OOM 报错 | OutOfMemoryError: PermGen space | OutOfMemoryError: Metaspace |
| 字符串常量池 | 在永久代(JDK 7 已移到堆) | 在堆 |
| 回收 | 随 Full GC,效果差 | 按类加载器卸载回收 |
为什么替换:
永久代的问题:
① 大小固定且难预估 → 设小了容易 OOM,设大了浪费
② 动态生成类的场景(反射、CGLIB 代理、JSP、动态语言)类信息暴涨 → 频繁 OOM
③ 和堆共享,永久代 GC 和堆 GC 纠缠,回收效率低
元空间的改进:
① 用本地内存,默认不设上限(受物理内存约束)→ 极少因大小不足 OOM
② 独立于堆,回收逻辑更清晰(按类加载器卸载)
元空间存什么:类的元信息——类的结构(字段、方法、字节码)、运行时常量池、方法数据等。注意对象实例仍在堆,元空间存的是「类的模板」不是「对象」。
⚠️ 「元空间用本地内存所以不会 OOM」是错的——它默认不设上限,但如果程序不断加载新类且旧类无法卸载(类加载器泄漏),元空间会一直增长直到耗尽本地内存,报
OutOfMemoryError: Metaspace。生产环境建议用-XX:MaxMetaspaceSize设一个上限,避免它无限吃内存拖垮整机。
完整版教学
一、类元信息要存在哪:永久代的由来
Java 程序运行时,除了对象(存堆里),还需要存放类的元信息——类叫什么、有哪些字段和方法、方法的字节码、常量池等。这些信息在类加载时读入,供 JVM 执行时查阅。
堆:存对象实例(new 出来的东西)
类元信息:存"类的模板"(类结构、方法、常量池)—— 存哪儿?
JDK 7 及以前:存在"永久代(PermGen)",是堆的一个特殊区域
永久代之所以叫「永久」,是因为早期设计认为类信息一旦加载就「永久存在、很少卸载」。但这个假设在现代应用里越来越站不住——大量框架动态生成类(Spring CGLIB 代理、Hibernate、JSP 编译、脚本引擎),类信息不断增长,「永久」的空间不够用了。这是永久代被淘汰的根源。
二、永久代的三宗罪
永久代的问题最终导致它被废弃:
罪1:大小固定且难预估
-XX:MaxPermSize 要提前设死。设小了 → 类一多就 OOM;
设大了 → 白白占用堆空间(永久代在堆内,挤占对象的空间)
而"到底该设多大"几乎无法准确预估
罪2:动态类场景频繁 OOM
反射生成类、CGLIB 动态代理、JSP 编译成类、Groovy 等脚本
→ 运行时不断产生新类 → 永久代持续增长 → OutOfMemoryError: PermGen space
(这是 JDK 7 时代最常见的 OOM 之一)
罪3:GC 效率低
永久代和堆共享,它的回收依附 Full GC,且类卸载条件苛刻、回收效果差
三宗罪的核心是「固定大小 + 在堆内 + 难以回收」。要根治,就得把类元信息从「堆内固定区域」搬出去——这就是元空间的思路。
三、元空间的核心改变:搬到本地内存
JDK 8 用元空间(Metaspace)替代永久代,最关键的一个改变是存储位置:
永久代:在 JVM 堆内 → 受 -Xmx 堆大小约束 → 空间有限、和对象抢地盘
元空间:在本地内存(堆外,即操作系统直接管理的内存)→ 默认只受物理内存限制
这一搬解决了大部分问题:① 不再和对象抢堆空间;② 默认不设固定上限(可以用满物理内存),所以极少因「元空间太小」而 OOM;③ 独立于堆,回收逻辑更清晰(元空间的回收以「类加载器」为单位——一个类加载器加载的所有类,在该加载器被回收时一起卸载)。所以从 JDK 8 开始,OutOfMemoryError: PermGen space 这个经典报错就基本消失了,改成了更少见的 Metaspace 报错。
四、元空间仍会 OOM:类加载器泄漏
一个严重误区是「元空间用本地内存,所以不会 OOM」。元空间仍会 OOM——它默认不设上限,但本地内存也是有限的:
元空间 OOM 的典型原因——类加载器泄漏:
正常:一个类加载器被回收 → 它加载的所有类卸载 → 元空间回收
泄漏:某个类加载器(及它加载的类)一直被引用,无法回收
→ 每次"热部署""动态生成类"都创建新加载器和新类,旧的又不释放
→ 元空间持续增长 → OutOfMemoryError: Metaspace
高发场景:
① 频繁热部署(每次部署一套新类,旧类加载器未释放)
② 大量动态代理/字节码增强(不断生成新类)
③ 用 ThreadLocal 或静态字段意外持有了类加载器引用
关键:类的卸载条件很严格——该类的所有实例都被回收、加载它的 ClassLoader 被回收、该类的 Class 对象没有被引用。三个条件缺一,类就无法卸载,元空间就回收不了。所以元空间 OOM 通常不是「类太多」,而是「该卸载的类因为加载器泄漏而卸载不掉」——这是排查思路的关键。
五、如何配置和排查元空间
虽然元空间默认不设上限,但生产环境建议显式设上限,避免它无限膨胀拖垮整机:
关键参数:
-XX:MaxMetaspaceSize=256m 设元空间上限(否则可能吃光物理内存)
-XX:MetaspaceSize=128m 初始阈值(达到后触发 Full GC 尝试卸载类并调整)
为什么要设 MaxMetaspaceSize:
不设 → 元空间无限增长,若有类加载器泄漏,会一直吃本地内存
→ 最终可能耗尽物理内存,导致整机变慢甚至其他进程被 OOM Killer 杀掉
设了 → 达到上限就抛 Metaspace OOM,把问题暴露在 JVM 层面(可控),
而不是拖垮整台机器
排查元空间 OOM:jcmd <pid> VM.metaspace 看元空间使用、-XX:+PrintGCDetails 看类加载/卸载数量、用 MAT 分析 dump 找「为什么类加载器无法回收」。核心是定位「哪个类加载器泄漏了、被谁引用着」。
六、几个相关的存储位置变迁
围绕永久代→元空间,还有几处存储位置的变化要理清(常被一起考):
| 内容 | JDK 7 | JDK 8+ |
|---|---|---|
| 类元信息 | 永久代 | 元空间(本地内存) |
| 运行时常量池 | 永久代 | 元空间 |
| 字符串常量池 | JDK 7 已从永久代移到堆 | 堆 |
| 静态变量 | JDK 7 已移到堆(Class 对象里) | 堆 |
一个高频考点:字符串常量池在 JDK 7 就已经从永久代挪到了堆(不是 JDK 8 才动的)。这是因为永久代空间小、字符串 intern() 多了容易撑爆永久代,所以先把字符串常量池移到堆。到 JDK 8 废弃永久代时,剩下的类元信息和运行时常量池才进入元空间。理清这个时间线,才不会把「字符串常量池位置」答错。
记忆钩子:「永久代在堆内、大小固定、易 OOM(PermGen space);元空间在本地内存、默认无上限、按类加载器卸载回收;但仍会 OOM(Metaspace)——类加载器泄漏导致类卸载不掉;生产要设 MaxMetaspaceSize;字符串常量池 JDK7 就移到堆了」。
七、常见误区与追问
- 误区:元空间用本地内存所以永远不会 OOM。 仍会——默认不设上限但本地内存有限,类加载器泄漏导致类无法卸载时,元空间会持续增长直到耗尽内存,报 Metaspace OOM。
- 误区:字符串常量池是 JDK 8 从永久代移到堆的。 JDK 7 就移到堆了;JDK 8 废弃永久代时移走的是类元信息和运行时常量池。
- 误区:元空间存对象实例。 元空间存类的元信息(类结构、方法、运行时常量池);对象实例始终在堆里。
- 误区:元空间不用设上限,反正用本地内存。 生产建议设 MaxMetaspaceSize,否则泄漏时会无限吃本地内存拖垮整机,设上限能把问题暴露在 JVM 层面。
- 追问:元空间 OOM 的根本原因通常是什么? 类加载器泄漏——某个 ClassLoader 及其加载的类一直被引用无法回收,配合频繁热部署/动态生成类,元空间持续增长,而非单纯类太多。
- 追问:类卸载需要满足什么条件? 该类所有实例已回收、加载它的 ClassLoader 已回收、该类的 Class 对象无引用;三者缺一类就无法卸载,元空间回收不了。
- 追问:为什么要用元空间替代永久代? 永久代大小固定难预估、在堆内挤占对象空间、动态类场景频繁 OOM、回收效率低;元空间用本地内存、默认无固定上限、按类加载器卸载,从根本上缓解了这些问题。
八、加强记忆
永久代(PermGen,≤JDK 7)存类元信息,问题是「在堆内、大小固定(MaxPermSize)、动态类场景易 OOM(PermGen space)、回收差」——反射/CGLIB/JSP 不断生成类时经常撑爆。JDK 8 用元空间(Metaspace) 替代,核心改变是搬到本地内存(堆外):不再和对象抢堆、默认只受物理内存限制、按类加载器卸载回收,于是经典的 PermGen OOM 基本消失。但要破除误解——元空间仍会 OOM(Metaspace):默认不设上限但本地内存有限,根本原因通常是类加载器泄漏(某 ClassLoader 及其类一直被引用无法卸载,配合频繁热部署/动态生成类,元空间持续膨胀)。类卸载条件很严(实例全回收 + 加载器回收 + Class 无引用),所以生产建议设 -XX:MaxMetaspaceSize 把问题暴露在 JVM 层、避免吃光整机内存。另外记住时间线:字符串常量池和静态变量在 JDK 7 就已从永久代移到堆,JDK 8 移走的是类元信息和运行时常量池。一句话「永久代固定在堆内易 OOM,元空间搬本地内存按加载器回收但仍会因加载器泄漏 OOM,生产设上限,字符串池 JDK7 就进堆了」。