JVM 第5讲:深入 JVM 方法区 —— 类的元数据之家
方法区(Method Area)是 JVM 运行时数据区中非常重要但又经常被误解的一个区域。它主要存储的是已经被虚拟机加载的类信息,也被称为“类的元数据之家”。
在不同 JDK 版本中,方法区的实现发生了很大的变化,我们需要分版本来理解。
1. 方法区的演变历史(重点掌握)
| JDK 版本 | 方法区实现方式 | 是否属于堆内存 | 是否有永久代(PermGen) | 实际名称(HotSpot) | 是否可动态扩展 | GC 方式(大致) |
|---|---|---|---|---|---|---|
| JDK 6 及之前 | 永久代(PermGen) | 是 | 有 | Permanent Generation | 固定大小 | Full GC 时回收 |
| JDK 7 | 永久代(部分移出) | 是 | 有(但字符串池移到堆) | Permanent Generation | 固定大小 | Full GC |
| JDK 8 及以后 | 元空间(Metaspace) | 否 | 无 | Metaspace | 动态扩展 | 触发 Metaspace GC(Full GC) |
最核心的变化点(面试高频):
- JDK 8 开始,永久代被彻底移除,元空间(Metaspace) 取代了它
- 元空间使用本地内存(Native Memory),不再受 -XX:MaxPermSize 限制
- 元空间默认情况下可以动态增长(受系统可用虚拟内存限制)
2. 方法区到底存放什么?(元数据内容)
无论永久代还是元空间,方法区主要存储以下内容:
- 类信息(Class Metadata)
- 类名、父类名、接口列表
- 字段(Field)信息:名称、类型、修饰符、属性值(static final 常量)
- 方法(Method)信息:名称、描述符、字节码、异常表、局部变量表、操作数栈大小等
- 运行时常量池(Runtime Constant Pool)
- Class 文件中常量池表的运行时表示
- 字面量(Literal):文本字符串、声明为 final 的基本类型常量
- 符号引用(Symbolic Reference):类和接口的全限定名、字段名和描述符、方法名和描述符
- 静态变量(类变量)
- static 修饰的变量(JDK 7 之前在永久代,JDK 7 开始移到普通堆)
- 对 JIT 编译器产生的代码缓存(部分版本)
注意:静态变量本身在 JDK 7 之后已经不在方法区了,而是放在堆的普通对象实例中(但 static final 常量仍可能留在运行时常量池)。
3. 运行时常量池(Runtime Constant Pool)详解
运行时常量池是方法区非常重要的一部分,也是最容易和“字符串常量池”混淆的区域。
包含内容:
- 字面量:如
"hello"、100、true - 符号引用:类、方法、字段的符号引用(在类加载的解析阶段会被解析为直接引用)
字符串常量池的变迁(常考):
| JDK 版本 | 字符串常量池位置 | 是否在方法区 | 说明 |
|---|---|---|---|
| JDK 6 及之前 | 永久代 | 是 | 字符串常量池在方法区 |
| JDK 7 | 堆(普通堆内存) | 否 | 永久代中移出字符串常量池 |
| JDK 8+ | 堆(元空间不包含字符串池) | 否 | 字符串常量池在堆中,元空间只存类元数据 |
经典面试题:
String s1 = "hello";
String s2 = new String("hello").intern();
System.out.println(s1 == s2); // true 或 false? 为什么?
- JDK 7+:
true(intern() 会把字符串放入字符串常量池,并返回引用)
4. 元空间(Metaspace) vs 永久代(PermGen)
| 对比项 | 永久代(PermGen) | 元空间(Metaspace) |
|---|---|---|
| 内存位置 | 堆内存 | 本地内存(Native Memory) |
| 大小固定吗 | 是(-XX:MaxPermSize) | 否(动态增长,默认无上限) |
| 容易 OOM 的场景 | 类加载过多、大量动态代理、大量反射 | 类加载非常多时也会耗尽系统虚拟内存 |
| GC 回收 | Full GC 时回收 | Metaspace Full GC |
| 调优参数 | -XX:PermSize -XX:MaxPermSize | -XX:MetaspaceSize -XX:MaxMetaspaceSize |
推荐调优参数(生产环境常见):
-XX:MetaspaceSize=256m # 初始触发 Full GC 的阈值(不是分配大小)
-XX:MaxMetaspaceSize=512m # 最大元空间大小(建议设置上限防止耗尽物理内存)
5. 方法区 / 元空间 OOM 的常见场景(真实案例)
- 大量动态代理 / CGLIB / JDK Proxy(Spring AOP、MyBatis 动态 mapper)
- 大量反射(如 JSON 框架大量使用反射)
- 大量类加载(OSGi、插件系统、热部署系统)
- 大量 JSP 编译(Tomcat 动态编译 JSP)
- Groovy / Scala 等动态语言特性
解决思路:
- 增大 Metaspace 大小(-XX:MaxMetaspaceSize)
- 优化代码,减少不必要的类加载
- 使用类加载隔离(如 tomcat 的 WebappClassLoader)
6. 总结:一句话记住方法区
方法区是存放已被加载的类元数据(类结构、方法字节码、运行时常量池等)的地方。
- JDK 6及之前:叫永久代,在堆里,固定大小
- JDK 8及之后:叫元空间,在本地内存,动态扩展
- 字符串常量池和 static 变量在 JDK7+ 已经移到普通堆
如果你现在想继续深入,可以告诉我你想重点看哪个方向:
- 运行时常量池的详细解析过程
- intern() 方法底层实现
- 类加载机制与方法区的关系
- 元空间 OOM 真实案例分析
- 方法区与堆、栈的对比图
随时说~