方法区与运行时常量池
方法区是线程共享区域,存放类元信息、运行时常量池、静态变量和 JIT 编译产物。JDK 8 起它从堆内的永久代迁出,由位于本地内存的**元空间(Metaspace)**实现,这是方法区历史上最大的一次变化。
永久代 → 元空间的演进
| 版本 | 实现位置 | 上限 | 问题 |
|---|---|---|---|
| JDK 7 | 永久代(堆内) | -XX:MaxPermSize | 大小难以预估,字符串常量池易撑爆永久代 |
| JDK 8+ | 元空间(本地内存) | 默认仅受系统内存约束 | 需用 MaxMetaspaceSize 兜底防失控 |
演进过程中的关键迁移:
- JDK 7:字符串常量池、静态变量从永久代移入堆
- JDK 8:整个永久代移除,类元信息等全部迁入元空间,新增
-XX:MetaspaceSize/-XX:MaxMetaspaceSize
元空间使用本地内存后,类加载数量不再受堆大小挤压,频繁动态生成类(如反射代理、热部署、CGLIB)导致的方法区 OOM 概率大幅下降。
方法区存储的内容
| 内容 | 说明 |
|---|---|
| 类型信息 | 类名、访问修饰符、父类、实现的接口 |
| 字段信息 | 字段名、类型、修饰符 |
| 方法信息 | 方法名、参数、返回值、访问标志、字节码、异常表 |
| 运行时常量池 | 编译期生成的字面量与符号引用 |
| 静态变量 | static 字段(JDK 7 后引用与对象存于堆,类型数据仍在方法区) |
| JIT 编译产物 | 热点方法编译后的机器码缓存(Code Cache) |
常量池:Class 常量池与运行时常量池
Class 文件常量池
.class 文件在编译期生成一张常量池表,存放字面量和符号引用。它属于静态描述,在类加载后交付给方法区。
| 类别 | 示例 |
|---|---|
| 字面量 | 字符串常量、final 常量值 |
| 符号引用 | 类和接口的全限定名、字段名称和描述符、方法名称和描述符 |
运行时常量池
类加载完成后,Class 文件常量池被载入方法区形成运行时常量池。它额外具备动态性:运行期可以通过 String.intern()、反射等方法向其中添加新的常量。
Class 文件常量池 --(类加载)--> 运行时常量池(方法区内)字符串常量池与 intern
字符串常量池(String Pool)是运行时常量池中的一部分。JDK 7 起它位于堆中,因此可以通过参数控制大小,也参与 GC。
java
String s1 = "hello"; // 编译期常量,入字符串常量池
String s2 = new String("hello"); // 堆上新对象,常量池中已有"hello"
String s3 = s2.intern(); // 若池中有则返回池中引用
s1 == s2 // false,一个是池中对象,一个是堆对象
s1 == s3 // true,intern 返回池中已有引用intern 行为要点:
"hello"这类字符串常量在编译期就进入常量池new String("hello")构造的是堆上的新对象,与池中对象不同intern()优先返回池中已有的引用;没有则入池(JDK 7 后为堆中引用入池)- 大量
intern()动态字符串会占用堆,可通过-XX:StringTableSize调大哈希表容量缓解冲突
常见问题
Metaspace OOM
java.lang.OutOfMemoryError: Metaspace常见诱因:
- 动态生成类未回收:CGLIB/ASM/反射代理类无限生成
- 类加载器泄漏:自定义 ClassLoader 不被回收,其加载的类永远留在元空间
- 大量 JSP/jsp 预编译、热部署未清理
排查思路:jstat -gcmetacapacity 观察元空间使用,jmap -clstats 查看类加载器统计,结合 MAT 分析类加载器引用链。
字符串常量池导致的堆 OOM
JDK 7 后字符串常量池在堆内,无界 intern() 会直接撑爆堆。排查时关注 String.intern() 调用点与 StringTableSize 配置。