引言:类信息存储在哪里?
当你写下一个类定义时:
public class User {
private static int count = 0;
private static final String TYPE = "USER";
private String name;
private int age;
public void sayHello() {
System.out.println("Hello");
}
}
这个类的信息(字段、方法、常量)存储在哪里?答案是:方法区(Method Area)。
方法区是JVM规范中定义的一个 逻辑概念,用于存储 类信息、常量、静态变量、即时编译器编译后的代码 等数据。
为什么方法区很重要?
- 存储类的元数据,是类加载的基础
- 运行时常量池是方法调用的关键
- JDK 8的元空间改造影响深远
- 方法区溢出是常见的OOM原因之一
什么是方法区?
核心定义
方法区(Method Area) 是《Java虚拟机规范》中定义的一个 逻辑概念,用于存储已被虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码 等数据。
关键特点:
- 线程共享:所有线程共享同一个方法区
- 逻辑概念:规范定义,具体实现由JVM决定
- 也称"非堆":与堆分开管理,但实际仍属于内存区域
- 可回收:方法区也会进行垃圾回收,但条件苛刻
方法区 vs 堆
| 对比维度 | 方法区 | 堆 |
|---|---|---|
| 存储内容 | 类信息、常量、静态变量 | 对象实例、数组 |
| 线程 | 线程共享 | 线程共享 |
| GC | 条件苛刻,效率低 | 频繁,效率高 |
| 溢出异常 | OutOfMemoryError: Metaspace/PermGen | OutOfMemoryError: Java heap space |
| 大小 | 较小(通常几十MB到几百MB) | 较大(通常几百MB到几GB) |
方法区存储的内容
方法区主要存储以下4类数据:
1️⃣ 类信息(Type Information)
定义:类的完整结构信息。
包含内容:
- 类的全限定名(如
java.lang.String) - 类的修饰符(public、final、abstract等)
- 直接父类的全限定名
- 实现的接口列表
- 类的字段信息(字段名、类型、修饰符)
- 类的方法信息(方法名、参数、返回值、修饰符、字节码、异常表等)
示例:
public class User extends Person implements Serializable {
private String name;
private int age;
public void sayHello() {
System.out.println("Hello");
}
}
存储在方法区的信息:
类名: com.example.User
修饰符: public
父类: com.example.Person
接口: java.io.Serializable
字段:
· name: String, private
· age: int, private
方法:
· sayHello(): void, public
· 字节码: [0: getstatic #2, 3: ldc #3, ...]
· 异常表: []
2️⃣ 运行时常量池(Runtime Constant Pool)
定义:存储编译期生成的 字面量 和 符号引用。
字面量(Literal):
- 字符串常量(如
"hello") - final常量值(如
final int MAX = 100) - 基本类型包装类的缓存值
符号引用(Symbolic Reference):
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
示例:
public class ConstantDemo {
public static void main(String[] args) {
String s1 = "hello"; // "hello"存储在运行时常量池
String s2 = "hello"; // 引用常量池中的同一个对象
System.out.println(s1 == s2); // true
}
}
常量池的作用:
- 节省内存:相同字符串只存储一份
- 提高效率:避免重复创建相同对象
- 支持动态链接:方法调用时解析符号引用
字符串常量池的演变
JDK 6及之前:字符串常量池在 方法区(永久代) JDK 7及之后:字符串常量池移到 堆 中
为什么要移动?
- 永久代大小固定,容易发生OOM
- 堆空间更大,更灵活
- 字符串对象与普通对象统一管理
验证代码:
public class StringPoolDemo {
public static void main(String[] args) {
String s1 = new String("hello");
String s2 = s1.intern(); // 将s1添加到字符串常量池
System.out.println(s1 == s2); // JDK 6: false, JDK 7+: true
}
}
3️⃣ 静态变量(Static Variables)
定义:类变量,使用 static 修饰的字段。
存储位置的演变:
- JDK 6及之前:静态变量存储在 方法区(永久代)
- JDK 7:静态变量移到 堆 中(随Class对象存储)
- JDK 8+:静态变量仍在 堆 中
示例:
public class StaticDemo {
private static int count = 0; // 基本类型静态变量
private static String name = "Java"; // 引用类型静态变量
private static final int MAX = 100; // 静态常量
public static void main(String[] args) {
count++;
System.out.println(count);
}
}
内存布局(JDK 8+):
方法区(元空间):
· StaticDemo类的元数据
· 方法的字节码
堆内存:
· StaticDemo的Class对象
· count = 0
· name = "Java"(引用)
· MAX = 100
· String对象 "Java"
4️⃣ 即时编译器编译后的代码(JIT-compiled Code)
定义:JIT编译器将热点代码编译为本地机器码,存储在方法区。
示例:
public class JITDemo {
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
hotMethod(); // 热点方法,会被JIT编译
}
}
public static void hotMethod() {
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
}
}
执行流程:
- 初次执行hotMethod时,解释执行字节码
- 多次调用后,JIT编译器将其编译为 本地机器码
- 编译后的代码存储在 方法区(Code Cache)
- 后续调用直接执行机器码,性能提升10-100倍
方法区的实现:永久代 vs 元空间
JDK 7及之前:永久代(PermGen)
定义:HotSpot虚拟机使用 永久代(Permanent Generation) 实现方法区。
特点:
- 使用 JVM堆内存 的一部分
- 大小固定(默认64MB,可通过参数调整)
- 容易发生 OutOfMemoryError: PermGen space
参数设置:
# 设置永久代初始大小为64MB
-XX:PermSize=64m
# 设置永久代最大大小为256MB
-XX:MaxPermSize=256m
永久代OOM示例:
/**
* VM参数:-XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class PermGenOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int i = 0;
while (true) {
// 不断生成字符串并调用intern(),填满永久代
list.add(String.valueOf(i++).intern());
}
}
}
输出:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
JDK 8及之后:元空间(Metaspace)
定义:JDK 8移除永久代,引入 元空间(Metaspace),使用 本地内存(Native Memory) 实现方法区。
元空间的优势:
- 使用本地内存:不再受堆大小限制
- 动态扩展:自动调整大小,减少OOM风险
- 简化GC:元空间的GC独立于堆GC
- 统一实现:JRockit和HotSpot统一内存管理
参数设置:
# 设置元空间初始大小为64MB
-XX:MetaspaceSize=64m
# 设置元空间最大大小为256MB
-XX:MaxMetaspaceSize=256m
# 不设置MaxMetaspaceSize,元空间可无限扩展(受限于物理内存)
元空间OOM示例:
/**
* VM参数:-XX:MaxMetaspaceSize=10M
*/
public class MetaspaceOOM {
public static void main(String[] args) {
int i = 0;
while (true) {
// 使用Cglib动态生成类,填满元空间
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Object.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
System.out.println("类生成: " + (++i));
}
}
}
输出:
类生成: 1234
类生成: 1235
...
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
永久代 vs 元空间对比
| 对比维度 | 永久代(PermGen)JDK 7及之前 | 元空间(Metaspace)JDK 8及之后 |
|---|---|---|
| 内存位置 | JVM堆内存的一部分 | 本地内存(Native Memory) |
| 大小限制 | 固定大小,需手动设置 | 动态扩展,可不设上限 |
| OOM风险 | 高(大小固定) | 低(自动扩展) |
| GC管理 | 与堆GC耦合 | 独立GC管理 |
| 参数设置 | -XX:PermSize-XX:MaxPermSize | -XX:MetaspaceSize-XX:MaxMetaspaceSize |
| OOM异常 | OutOfMemoryError: PermGen space | OutOfMemoryError: Metaspace |
方法区的垃圾回收
回收目标
方法区的垃圾回收主要针对两类对象:
- 废弃的常量
- 无用的类
1️⃣ 废弃常量的回收
判定条件:常量池中的常量没有任何引用。
示例:
String s1 = "hello";
s1 = null; // "hello"可能被回收(如果没有其他引用)
回收条件:
- 堆中没有任何String对象引用该常量
- 字符串常量池中没有其他引用
2️⃣ 无用类的回收
判定条件(必须同时满足):
- 该类的所有实例都已被回收(堆中不存在该类的任何实例)
- 加载该类的类加载器已被回收(非常难达成)
- 该类对应的Class对象没有被引用(无法通过反射访问该类)
示例:
public class ClassUnloadDemo {
public static void main(String[] args) throws Exception {
// 使用自定义类加载器加载类
MyClassLoader loader = new MyClassLoader();
Class<?> clazz = loader.loadClass("com.example.MyClass");
// 创建实例
Object obj = clazz.newInstance();
// 释放所有引用
obj = null;
clazz = null;
loader = null;
// 建议执行GC(不保证立即执行)
System.gc();
// 此时MyClass可能被卸载(取决于JVM实现)
}
}
关键理解:
- 类的卸载条件非常苛刻
- 应用类加载器(AppClassLoader)通常不会被回收
- 只有自定义类加载器加载的类才可能被卸载
常见问题与误区
❌ 误区1:方法区不会发生OOM
真相:方法区也会发生OutOfMemoryError。
常见场景:
- 加载大量类(如使用Cglib、动态代理)
- JSP页面过多(每个JSP编译为一个类)
- 字符串常量池溢出(JDK 6及之前)
❌ 误区2:方法区不进行垃圾回收
真相:方法区会进行垃圾回收,但效率很低。
回收条件苛刻:
- 类卸载需要满足3个条件
- 常量回收相对简单,但也需要无引用
❌ 误区3:静态变量存储在方法区
真相(JDK 7及之后):
- 静态变量存储在 堆 中(随Class对象)
- 方法区只存储 类的元数据
实战建议
元空间大小设置
默认配置:
- 初始大小:约20MB
- 最大大小:无限制(受限于物理内存)
推荐配置:
# 中小型应用
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m
# 大型应用(大量类)
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
# 微服务应用(类较少)
-XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=128m
避免元空间OOM
预防措施:
- 限制动态类生成:谨慎使用Cglib、动态代理
- 合理设置MetaspaceSize:根据应用规模调整
- 监控元空间使用:使用JConsole、VisualVM监控
- 定期重启应用:如果类加载器无法回收,定期重启释放内存
总结
核心要点
方法区是线程共享的逻辑概念,存储类信息、常量、静态变量、JIT代码
JDK 8移除永久代,引入元空间,使用本地内存,动态扩展
字符串常量池和静态变量从方法区移到堆(JDK 7+)
方法区也会进行垃圾回收,但条件苛刻,效率低
元空间OOM的主要原因是动态类生成过多
与下篇文章的衔接
下一篇文章,我们将学习 直接内存(Direct Memory),理解堆外内存的使用场景、NIO的零拷贝机制,以及直接内存的管理。
参考资料
- 《深入理解Java虚拟机(第3版)》- 周志明
- JEP 122: Remove the Permanent Generation
- Java虚拟机规范(Java SE 8版)
下一篇预告:《直接内存:堆外内存与NIO》 深入理解直接内存的作用、NIO的DirectByteBuffer、零拷贝机制,以及堆外内存的管理。