方法区与元空间
方法区用于存储类相关信息。它是 JVM 规范中的概念,HotSpot 在不同 JDK 版本中有不同实现:JDK 8 之前主要是永久代,JDK 8 之后使用元空间。
# 1. 方法区存什么
方法区主要存储:
- 类的元数据。
- 字段信息。
- 方法信息。
- 常量信息。
- 静态变量相关数据。
- 运行时常量池。
它服务于类加载和方法执行。
# 2. 永久代
在 JDK 8 之前,HotSpot 使用永久代实现方法区。
永久代使用 JVM 堆内或受 JVM 管理的一块内存,大小通常通过参数控制:
-XX:PermSize
-XX:MaxPermSize
永久代常见问题:
java.lang.OutOfMemoryError: PermGen space
常见于大量类加载、动态代理、热部署和应用服务器类加载器泄漏。
# 3. 元空间
JDK 8 移除了永久代,改用元空间实现方法区。
元空间使用本地内存,常见参数:
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
如果不限制最大值,元空间可能受可用本地内存限制。
元空间不足可能出现:
java.lang.OutOfMemoryError: Metaspace
# 4. 为什么移除永久代
永久代大小较难估计,容易因为类元数据增长导致 OOM。元空间使用本地内存,可以更灵活地管理类元数据。
但这不代表元空间不会 OOM。大量动态生成类、类加载器泄漏仍然会耗尽元空间。
# 5. 运行时常量池
运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
例如:
- 字符串字面量。
- 类和方法符号引用。
- 字段符号引用。
类加载后,Class 文件常量池会进入运行时常量池。
# 6. 字符串常量池
字符串常量池和运行时常量池相关,但在 HotSpot 中其具体位置随版本变化。
JDK 7 之后,字符串常量池移到了堆中。
String a = "hello";
String b = "hello";
System.out.println(a == b); // true
字面量字符串会复用常量池中的对象。
# 7. 元空间问题排查
排查方向:
- 是否大量动态生成类。
- 是否频繁创建新的类加载器。
- 是否存在热部署未释放。
- 是否大量使用 CGLIB、ByteBuddy、Javassist。
- 是否有脚本引擎或规则引擎动态编译。
可以结合类加载日志、jcmd VM.classloader_stats、堆转储分析。
# 8. 阶段小结
方法区是规范概念,永久代和元空间是 HotSpot 的实现。JDK 8 后,元空间取代永久代,但类元数据依然需要内存。类加载器泄漏和动态生成类仍然可能导致元空间 OOM。
# 9. 类元数据进入元空间的流程
ClassLoader 读取 class 字节流
│
▼
验证、准备、解析
│
▼
在元空间保存类元数据
│
├─ 类名、父类、接口
├─ 字段表
├─ 方法表
├─ 常量池运行时结构
└─ 注解、泛型签名等属性
│
▼
堆中生成 java.lang.Class 对象
注意:Class 对象在堆中,类元数据主要在元空间中。二者有关联,但不是同一个东西。
# 10. 元空间增长的常见来源
| 来源 | 说明 |
|---|---|
| 应用启动加载大量类 | 正常增长 |
| Spring/CGLIB 动态代理 | 生成代理类 |
| MyBatis、Hibernate 等框架 | 可能生成辅助类或代理类 |
| Groovy、Janino、规则引擎 | 动态编译类 |
| 热部署 | 新类加载器加载新版本类 |
| 插件系统 | 每个插件可能有独立类加载器 |
正常应用启动后,类加载数量应该趋于稳定。如果运行期间类数量持续增长,就要怀疑动态生成类或类加载器泄漏。
# 11. 类加载器泄漏图解
GC Roots
│
▼
某个静态变量 / 线程 / ThreadLocal / 注册表
│
▼
旧 WebAppClassLoader
│
▼
它加载的所有 Class
│
▼
元空间中的类元数据无法释放
热部署场景中,如果旧类加载器没有释放,旧版本应用的类元数据会一直留在元空间。
# 12. Tips 快问快答
Q:元空间在堆里吗?
A:HotSpot JDK 8+ 中元空间使用本地内存,不在 Java 堆里。
Q:MaxMetaspaceSize 不设置可以吗?
A:可以,但生产环境建议结合容器总内存设置上限,避免元空间无限增长导致进程内存压力。
Q:字符串常量池在元空间吗?
A:HotSpot JDK 7 之后字符串常量池主要在堆中,Class 文件常量池运行时结构和方法区相关。
Q:元空间 OOM 怎么排查?
A:看类加载数量、类加载器数量、动态代理/脚本生成、热部署和 jcmd VM.classloader_stats。