如果你只想记一个结论:
- JDK 1.7 :有永久代(
PermGen) - JDK 1.8 :移除永久代,用元空间(
Metaspace,走本地内存)
但只记结论不够,因为你线上会遇到:
OutOfMemoryError: PermGen spaceOutOfMemoryError: Metaspace- 动态代理/反射/生成类太多导致内存异常
这篇把"方法区是什么、放什么、为什么改、怎么排"讲透。
1. 方法区是什么:放"类本身的信息"的地方
方法区是 JVM 规范里的一个概念,主要存:
- 类元数据:类结构、字段/方法信息、访问标志等
- 运行时常量池:字符串常量、符号引用等
- 静态变量相关信息(具体实现上会有差异)
- JIT 相关的一些元数据(不同实现有所不同)
一句话:
- 堆里放对象
- 方法区(元空间)放类
2. JDK 1.7:永久代(PermGen)
2.1 它为什么容易 OOM
永久代属于 HotSpot 的实现细节(不是 JVM 规范硬规定),它的问题在于:
- 空间大小受 JVM 参数限制(配置不当很容易顶满)
- 类卸载条件苛刻(尤其在容器里反复热部署)
典型场景:
- Tomcat 反复热部署,类加载器泄露
- 大量动态生成类(CGLIB、ASM、Javassist)
- 运行时常量池/字符串占用膨胀
2.2 常见参数(JDK 1.7)
-XX:PermSize-XX:MaxPermSize
3. JDK 1.8:元空间(Metaspace)
3.1 为什么要改
把类元数据从永久代迁到元空间,核心动机:
- 永久代太容易 OOM,且难以调优
- 类元数据更适合放到本地内存,避免和 Java 堆争抢
3.2 元空间还会 OOM 吗
会。
- 元空间用的是本地内存,不等于无限
- 如果你不设上限,可能把机器内存吃光
- 如果你设了上限,达到上限一样会
OutOfMemoryError: Metaspace
3.3 常见参数(JDK 1.8)
-XX:MetaspaceSize(触发扩容/GC 的阈值,不是硬上限)-XX:MaxMetaspaceSize(硬上限)
4. 运行时常量池:很多人混淆的点
你会听到"常量池在方法区里",但要注意几个历史变化:
- 规范层面:运行时常量池属于方法区的一部分
- HotSpot 实现上,JDK 版本间有多次迁移/调整
工程上你记住就行:
- 类加载越多、常量越多,方法区/元空间压力越大
5. 实战:怎么判断是 Metaspace 泄露还是堆泄露
5.1 快速判断现象
- 堆泄露:
Java heap space,对象一直涨,Full GC 也回不去 - 元空间泄露:
Metaspace,类数量/类元数据一直涨
5.2 常用观测(低侵入)
- 看类直方图:
jcmd <pid> GC.class_histogram - 看类加载统计:
jcmd <pid> VM.classloaders
你重点盯两件事:
- 类数量是否持续增长
- 是否存在"旧类加载器"长期存活(热部署/插件系统常见)
6. 一个常见根因:类加载器泄露
"类卸载"发生的前提通常是:
- 对应的
ClassLoader变成不可达
如果你有:
- 静态集合缓存了某个类加载器加载的对象
- 线程本地变量(
ThreadLocal)引用了它 - 第三方库注册监听器但不注销
就会导致:
- 类加载器无法回收
- 其加载的所有类元数据也无法卸载
- 最终
Metaspace OOM
7. 总结
- 方法区(元空间)放"类",堆放"对象"
- JDK 1.7 的
PermGen改成 JDK 1.8 的Metaspace Metaspace也会 OOM,关键看类是否不断增加、类加载器是否泄露- 排障优先从
class_histogram和classloaders入手