问题
方法区存储什么数据?
答案
核心概念
方法区(Method Area)是JVM规范定义的逻辑概念,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。它是线程共享的内存区域,在JVM启动时创建。
方法区存储的数据类型
1. 类型信息(Type Information)
每个加载的类(class)、接口(interface)、枚举(enum)、注解(annotation)都会在方法区存储以下信息:
A. 类的基本信息:
// 示例类
public class User extends Person implements Serializable {
// ...
}
// 方法区存储:
// - 类的完全限定名: com.example.User
// - 父类的完全限定名: com.example.Person
// - 类的访问修饰符: public
// - 类的类型: class (还可能是interface/enum/annotation)
// - 实现的接口列表: [java.io.Serializable]
// - 类的版本信息(major/minor version)
B. 类的结构信息:
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
}
// 方法区存储:
// 1. 字段信息:
// - 字段名: name, age
// - 字段类型: String, int
// - 字段修饰符: private
// - 字段的字节码偏移量
//
// 2. 方法信息:
// - 方法名: <init>, getName
// - 方法返回类型: void, String
// - 方法参数类型: (String, int), ()
// - 方法修饰符: public
// - 方法的字节码(code)
// - 操作数栈深度
// - 局部变量表大小
// - 异常表
C. 类的继承关系:
// 类的继承树信息
class Animal {}
class Dog extends Animal {}
class Husky extends Dog {}
// 方法区存储:
// - Husky -> Dog -> Animal -> Object 的继承链
// - 用于方法查找、类型检查(instanceof)
2. 运行时常量池(Runtime Constant Pool)
运行时常量池是方法区的一部分,是class文件中常量池表(Constant Pool Table)的运行时表示。
A. 字面量常量:
public class ConstantExample {
private static final int MAX = 100; // 整型常量
private static final String NAME = "Java"; // 字符串常量
private static final double PI = 3.14159; // 浮点常量
private static final boolean FLAG = true; // 布尔常量
}
// 运行时常量池存储:
// - 100 (整型字面量)
// - "Java" (字符串字面量的引用,实际字符串对象在堆中)
// - 3.14159 (双精度浮点字面量)
// - true (布尔字面量)
B. 符号引用:
public class ReferenceExample {
private User user;
public void method() {
user.getName();
}
}
// 运行时常量池存储:
// 1. 类的全限定名: com/example/User (CONSTANT_Class_info)
// 2. 字段名称和描述符: user:Lcom/example/User; (CONSTANT_Fieldref_info)
// 3. 方法名称和描述符: getName:()Ljava/lang/String; (CONSTANT_Methodref_info)
//
// 类加载时,符号引用会被解析为直接引用(内存地址)
C. 动态常量:
// JDK 11+ 支持的动态常量(CONSTANT_Dynamic)
// 通过invokedynamic指令在运行时计算
3. 字段信息(Field Information)
实例字段:
public class Book {
private String title; // 实例字段
private String author;
private double price;
private int pages;
}
// 方法区存储字段元数据:
// - 字段名称: title, author, price, pages
// - 字段类型描述符: Ljava/lang/String;, Ljava/lang/String;, D, I
// - 字段访问标志: ACC_PRIVATE
// - 字段在对象中的偏移量(用于快速访问)
静态字段(JDK 7之前在方法区,JDK 7+移至堆中):
public class Config {
public static String APP_NAME = "MyApp"; // 静态变量
public static final int VERSION = 1; // 静态常量
private static Connection connection; // 静态对象引用
}
// JDK 6及以前: 静态变量存储在方法区(PermGen)
// JDK 7及以后: 静态变量移至堆中,方法区只保留元数据
4. 方法信息(Method Information)
方法元数据:
public int calculate(int a, int b) throws IllegalArgumentException {
if (a < 0 || b < 0) {
throw new IllegalArgumentException("参数不能为负数");
}
return a + b;
}
// 方法区存储:
// 1. 方法名称: calculate
// 2. 方法描述符: (II)I // (int, int) -> int
// 3. 访问标志: ACC_PUBLIC
// 4. 方法字节码指令序列:
// 0: iload_1 // 加载参数a
// 1: ifge 12 // 如果a >= 0跳转到12
// 4: new #2 // 创建IllegalArgumentException
// ...
// 5. 异常表:
// from to target type
// 0 12 15 IllegalArgumentException
// 6. 局部变量表大小: 3 (this, a, b)
// 7. 操作数栈最大深度: 2
// 8. 行号表(用于调试):
// line 10: 0
// line 11: 4
// ...
方法的Code属性:
// 每个方法(除abstract/native外)都有Code属性
// 包含:
// - max_stack: 操作数栈最大深度
// - max_locals: 局部变量表最大长度
// - code[]: 字节码指令数组
// - exception_table[]: 异常处理表
// - attributes[]: 其他属性(LineNumberTable, LocalVariableTable等)
5. 即时编译器编译后的代码缓存(Code Cache)
JIT编译代码:
// 热点代码会被JIT编译为本地机器码
public int hotMethod(int n) {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += i;
}
return sum;
}
// 执行流程:
// 1. 最初: 解释执行字节码
// 2. 达到阈值(-XX:CompileThreshold): 触发JIT编译
// 3. 编译后: 本地机器码存储在Code Cache
// 4. 后续: 直接执行本地机器码(性能提升10-100倍)
Code Cache配置:
# 查看Code Cache使用情况
jinfo -flag ReservedCodeCacheSize <pid>
# 配置Code Cache大小
-XX:ReservedCodeCacheSize=240m # 默认约240MB
-XX:InitialCodeCacheSize=128m # 初始大小
# 监控Code Cache
jstat -compiler <pid> # 查看编译统计
6. 其他重要信息
A. 类的常量池表:
// class文件的Constant Pool在加载后成为运行时常量池
// 包含:
// - CONSTANT_Utf8: 字符串字面量
// - CONSTANT_Integer/Float/Long/Double: 数值常量
// - CONSTANT_Class: 类或接口的符号引用
// - CONSTANT_String: 字符串常量的符号引用
// - CONSTANT_Fieldref: 字段的符号引用
// - CONSTANT_Methodref: 方法的符号引用
// - CONSTANT_InterfaceMethodref: 接口方法的符号引用
B. 类的属性(Attributes):
// 类级别的属性存储在方法区:
// - SourceFile: 源文件名
// - InnerClasses: 内部类信息
// - Signature: 泛型签名
// - Annotations: 注解信息
// - BootstrapMethods: invokedynamic指令的引导方法
@Deprecated
@MyCustomAnnotation(value = "test")
public class Example {
// 注解信息存储在方法区
}
版本差异
JDK 7及以前: 永久代(PermGen)
# 永久代配置
-XX:PermSize=128m # 初始大小
-XX:MaxPermSize=256m # 最大大小
# 永久代存储:
# - 类元数据
# - 运行时常量池
# - 字符串常量池 (JDK 6在PermGen, JDK 7移至堆)
# - 静态变量 (JDK 7移至堆)
JDK 8及以后: 元空间(Metaspace)
# 元空间配置
-XX:MetaspaceSize=128m # 初始大小
-XX:MaxMetaspaceSize=512m # 最大大小(默认无限制)
# 元空间存储:
# - 类元数据 (在本地内存中)
# - 运行时常量池(不含字符串常量池)
# - Code Cache (独立区域)
# 注意:
# - 字符串常量池在堆中
# - 静态变量在堆中
实际示例
public class MethodAreaExample {
// 类信息: MethodAreaExample, extends Object, public
// 字段信息
private static int staticVar = 10; // 静态变量(JDK7+在堆)
private String instanceVar = "hello"; // 实例字段元数据在方法区
// 常量信息
public static final String CONSTANT = "CONST"; // 常量池
// 方法信息
public void method() {
// 方法字节码存储在方法区
String local = "local";
System.out.println(local);
}
// Native方法
public native void nativeMethod();
public static void main(String[] args) {
// 触发类加载,方法区创建MethodAreaExample类的元数据
MethodAreaExample example = new MethodAreaExample();
example.method();
}
}
// 方法区存储内容总结:
// 1. 类型信息: MethodAreaExample的类元数据
// 2. 字段信息: staticVar, instanceVar的元数据
// 3. 方法信息: method, nativeMethod, main的字节码和元数据
// 4. 常量池: "CONST", "hello", "local"的符号引用
// 5. 符号引用: System, String, PrintStream等的引用
查看方法区数据
# 1. 查看加载的类
jcmd <pid> VM.classes
# 2. 查看常量池
javap -v MethodAreaExample.class
# 输出示例:
# Constant pool:
# #1 = Methodref #6.#20 // java/lang/Object."<init>":()V
# #2 = String #21 // hello
# #3 = Fieldref #5.#22 // MethodAreaExample.instanceVar:Ljava/lang/String;
# ...
# 3. 查看元空间使用情况
jstat -gcmetacapacity <pid>
# 4. 使用JConsole/VisualVM查看
# Memory -> Metaspace
方法区的GC
方法区也会进行垃圾回收,主要回收:
- 废弃的常量: 没有任何对象引用的常量
- 不再使用的类: 满足以下条件的类可以被卸载
- 该类所有实例已被回收
- 加载该类的ClassLoader已被回收
- 该类的java.lang.Class对象没有被引用
# 启用类卸载日志
-XX:+TraceClassUnloading
-verbose:class
# 输出示例:
# [Unloading class com.example.DynamicClass]
面试总结
方法区主要存储:
- 类型信息:
- 类的全限定名、父类、接口
- 类的修饰符、类型(class/interface/enum)
- 字段信息:
- 字段名称、类型、修饰符
- 字段元数据(偏移量等)
- 方法信息:
- 方法名称、返回类型、参数
- 方法字节码、异常表
- 局部变量表大小、操作数栈深度
- 运行时常量池:
- 字面量常量(数值、字符串引用)
- 符号引用(类、字段、方法)
- 即时编译代码缓存:
- JIT编译的本地机器码
版本变化:
- JDK 7: 字符串常量池、静态变量移至堆
- JDK 8: 永久代移除,改用元空间(本地内存)
关键点:
- 方法区是逻辑概念,永久代/元空间是实现
- 存储类的”元数据”,不是类的实例数据(实例在堆上)
- 方法区也会GC,回收废弃常量和不再使用的类