引言:类信息存储在哪里?

当你写下一个类定义时:

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/PermGenOutOfMemoryError: 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;
        }
    }
}

执行流程

  1. 初次执行hotMethod时,解释执行字节码
  2. 多次调用后,JIT编译器将其编译为 本地机器码
  3. 编译后的代码存储在 方法区(Code Cache)
  4. 后续调用直接执行机器码,性能提升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) 实现方法区。

元空间的优势

  1. 使用本地内存:不再受堆大小限制
  2. 动态扩展:自动调整大小,减少OOM风险
  3. 简化GC:元空间的GC独立于堆GC
  4. 统一实现: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 spaceOutOfMemoryError: Metaspace

方法区的垃圾回收

回收目标

方法区的垃圾回收主要针对两类对象:

  1. 废弃的常量
  2. 无用的类

1️⃣ 废弃常量的回收

判定条件:常量池中的常量没有任何引用。

示例

String s1 = "hello";
s1 = null;  // "hello"可能被回收(如果没有其他引用)

回收条件

  • 堆中没有任何String对象引用该常量
  • 字符串常量池中没有其他引用

2️⃣ 无用类的回收

判定条件(必须同时满足):

  1. 该类的所有实例都已被回收(堆中不存在该类的任何实例)
  2. 加载该类的类加载器已被回收(非常难达成)
  3. 该类对应的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

预防措施

  1. 限制动态类生成:谨慎使用Cglib、动态代理
  2. 合理设置MetaspaceSize:根据应用规模调整
  3. 监控元空间使用:使用JConsole、VisualVM监控
  4. 定期重启应用:如果类加载器无法回收,定期重启释放内存

总结

核心要点

  1. 方法区是线程共享的逻辑概念,存储类信息、常量、静态变量、JIT代码

  2. JDK 8移除永久代,引入元空间,使用本地内存,动态扩展

  3. 字符串常量池和静态变量从方法区移到堆(JDK 7+)

  4. 方法区也会进行垃圾回收,但条件苛刻,效率低

  5. 元空间OOM的主要原因是动态类生成过多

与下篇文章的衔接

下一篇文章,我们将学习 直接内存(Direct Memory),理解堆外内存的使用场景、NIO的零拷贝机制,以及直接内存的管理。


参考资料


下一篇预告:《直接内存:堆外内存与NIO》 深入理解直接内存的作用、NIO的DirectByteBuffer、零拷贝机制,以及堆外内存的管理。