Skip to content

方法区概述(与栈/堆交互)

方法区:类元信息的仓库

方法区(Method Area)是 JVM 规范中定义的线程共享的内存区域,用于存储已被加载的类信息常量静态变量JIT 编译后的代码等。

如果说堆是对象的家,那方法区就是类的家——类的结构、方法的字节码、字段定义,都存在这里。

方法区存什么

┌──────────────────────────────────────────────────────┐
│                    方法区                              │
│                                                      │
│  ┌──────────────────────────────────────────────┐   │
│  │  类型信息                                        │   │
│  │  - 类的全限定名                                 │   │
│  │  - 父类的全限定名                               │   │
│  │  - 修饰符(public/abstract/final...)         │   │
│  │  - 是否是接口                                    │   │
│  └──────────────────────────────────────────────┘   │
│  ┌──────────────────────────────────────────────┐   │
│  │  字段信息                                        │   │
│  │  - 字段名、类型、修饰符                         │   │
│  │  - 是否是常量(static final)                    │   │
│  └──────────────────────────────────────────────┘   │
│  ┌──────────────────────────────────────────────┐   │
│  │  方法信息                                        │   │
│  │  - 方法名、返回类型、参数列表                    │   │
│  │  - 修饰符                                       │   │
│  │  - 方法的字节码                                 │   │
│  │  - 操作数栈最大深度、局部变量表大小             │   │
│  │  - 异常表                                        │   │
│  └──────────────────────────────────────────────┘   │
│  ┌──────────────────────────────────────────────┐   │
│  │  运行时常量池                                    │   │
│  │  - 字符串常量                                   │   │
│  │  - 数值常量                                     │   │
│  │  - 类/方法/字段的符号引用                        │   │
│  └──────────────────────────────────────────────┘   │
│  ┌──────────────────────────────────────────────┐   │
│  │  JIT 编译后的代码缓存                           │   │
│  │  - JIT 编译器生成的本地机器码                  │   │
│  └──────────────────────────────────────────────┘   │
│  ┌──────────────────────────────────────────────┐   │
│  │  域引用、方法表、内部结构等                      │   │
│  └──────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────┘

方法区与栈、堆的关系

方法区、堆、栈是 JVM 内存模型的三驾马车,它们之间有密切的交互:

方法区 ↔ 堆:对象的类型信息

每个堆中的对象都知道自己是什么类型。这个「知道」是通过对象头中的类型指针实现的:

java
public class HeapObject {
    private int value;

    public static void main(String[] args) {
        HeapObject obj = new HeapObject();
        // obj 是堆中的对象
        // obj 的类型指针指向方法区中的 HeapObject 类信息
        // 通过 obj.getClass() 可以访问到方法区中的类元数据
        Class<?> clazz = obj.getClass();
        System.out.println(clazz.getName());  // HeapObject
        // clazz 指向方法区中该类的 Class 对象
    }
}

方法区 ↔ 栈:方法的执行

栈帧的动态链接指向方法区中的运行时常量池

java
public class StackMethod {
    public int compute() {
        // compute() 方法的字节码在方法区中
        // 栈帧中的动态链接,通过符号引用访问方法区
        return 1 + 2;
    }
    // 调用时:
    // - 方法字节码 → 方法区
    // - 局部变量表 → 当前栈帧
    // - 操作数栈 → 当前栈帧
}

三者的数据流向

方法区 ──类信息──→ 堆中对象
  │                        │
  │ 动态链接                │ 类型指针
  │                        │
  ▼                        ▼
栈帧 ──────────→ 操作数栈 + 局部变量表

方法区的演进:PermGen → Metaspace

方法区是 JVM 规范中的一个逻辑概念,但实现方式在 JDK 8 发生了重大变化

维度JDK 7 及之前JDK 8+
实现PermGen(永久代)Metaspace(元空间)
位置堆内(受 -Xmx 控制)本地内存(不受堆大小限制)
大小限制受 -XX:PermSize/-XX:MaxPermSize 控制受 -XX:MaxMetaspaceSize 控制
OOM 原因永久代满(类太多/常量太多)元空间满(加载类太多)
JDK 7:
┌─────────────────────────────────────────┐
│                    堆(Heap)              │
│  ┌───────────┐      ┌────────────────┐ │
│  │ 年轻代    │      │ 老年代          │ │
│  └───────────┘      └────────────────┘ │
│  ┌────────────────────────────────────┐ │
│  │        永久代(PermGen)            │ │
│  │  类信息、常量池、字符串常量          │ │
│  └────────────────────────────────────┘ │
└─────────────────────────────────────────┘

JDK 8+:
┌─────────────────────────────────────────┐
│                    堆(Heap)              │
│  ┌───────────┐      ┌────────────────┐ │
│  │ 年轻代    │      │ 老年代          │ │
│  └───────────┘      └────────────────┘ │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│               本地内存(Native Memory)     │
│  ┌────────────────────────────────────┐ │
│  │        元空间(Metaspace)          │ │
│  │  类信息、常量池、代码缓存            │ │
│  └────────────────────────────────────┘ │
└─────────────────────────────────────────┘

为什么移除 PermGen

永久代(PermGen)有几个根本性问题:

  1. 大小受堆限制:PermGen 是堆的一部分,设置小了容易 OOM,设置大了影响堆
  2. 需要完整的 GC:虽然叫「永久」代,但类也会被卸载(需要 Full GC)
  3. 字符串常量池冲突:JDK 7 把字符串常量池从 PermGen 移到了堆,但空间分配仍然紧张

JDK 8 的元空间方案彻底解决了这些问题。

方法区是线程共享的

方法区是被所有线程共享的内存区域。这意味着:

java
public class MethodAreaSharing {
    public static void main(String[] args) {
        // Thread A 和 Thread B 共享同一个方法区
        // 类信息只加载一份,节省内存
        new ThreadA().start();
        new ThreadB().start();
    }
}

本节小结

方法区的核心要点:

关键点说明
存储内容类信息、字段信息、方法信息、运行时常量池、代码缓存
线程共享所有线程共用同一方法区
生命周期与 JVM 进程相同
JDK 7 之前PermGen 实现,位于堆内
JDK 8+Metaspace 实现,位于本地内存
OOM 原因元空间满(加载类太多)

方法区与堆、栈共同构成了 JVM 的内存布局,理解它们的交互关系是理解整个 JVM 运行机制的关键。

下一节,我们来看 HotSpot 方法区演进(JDK6/7/8),深入理解方法区的演进历程。

基于 VitePress 构建