0%

方法区

JVM 方法区:类元数据的存储中心

方法区(Method Area)是 JVM 运行时数据区的重要组成部分,虽在逻辑上属于堆的一部分,但在实现上通常被视为独立于堆的内存区域。它主要用于存储类的元数据、常量、静态变量等关键信息,是支撑 Java 反射、动态加载等特性的核心区域。本文将详细解析方法区的演变、存储内容、垃圾回收及参数配置,帮助理解其在 JVM 中的作用。

方法区的基本特性

线程共享与生命周期

  • 线程共享:方法区是所有线程共享的内存区域,存储的类信息、常量等被所有线程共同访问。
  • 生命周期:随 JVM 实例启动而创建,随 JVM 退出而销毁,与 JVM 生命周期一致。

核心功能

方法区的核心作用是存储已被 JVM 加载的类的元数据及相关信息,包括:

  • 类的结构信息(如类名、父类、接口);
  • 常量池(字面量、符号引用);
  • 静态变量;
  • 方法的字节码、局部变量表结构等。

与堆的关系

  • 逻辑上,方法区属于堆的一部分(JVM 规范将其描述为 “堆的逻辑分区”);
  • 实现上,方法区被称为 “非堆”(Non-Heap),以区分于存储对象实例的堆,且垃圾回收策略与堆不同。

方法区的演变:从永久代到元空间

方法区的实现随 JDK 版本演变,核心变化是从 “永久代” 转向 “元空间”,解决了永久代内存限制的问题:

版本 实现方式 内存来源 核心问题
JDK 7 及以前 永久代(Permanent Generation) JVM 堆内存 容量固定,易因类过多导致 PermGen space OOM
JDK 8 及以后 元空间(Metaspace) 本地内存(直接内存) 内存上限为系统可用内存,减少 OOM 风险

关键变化点:

  1. JDK 7 的调整
    • 字符串常量池静态变量从永久代移至堆中,永久代仅保留类元数据、运行时常量池等。
  2. JDK 8 的重构
    • 彻底移除永久代,改用元空间实现方法区;
    • 元空间存储类元数据、运行时常量池等,依赖本地内存(不占用 JVM 堆内存),默认无固定上限(可通过参数限制)。

方法区存储的核心内容

方法区的存储内容随 JDK 版本略有差异,但核心是类加载后的元数据。以 JDK 8 为例,主要包括以下内容:

1. 类信息(Class Metadata)

当类加载器加载 .class 文件后,方法区会存储该类的基本结构信息,包括:

  • 类的完整名称(如 com.example.User);
  • 直接父类的完整名称(如 java.lang.Object,接口无父类);
  • 修饰符(如 publicabstractfinal);
  • 直接接口列表(按声明顺序存储)。

2. 域信息(Field 信息)

域(字段)是类中定义的变量,方法区存储每个域的详细信息:

  • 域名称(如 nameage);
  • 域类型(如 Stringint);
  • 修饰符(如 publicprivatestaticfinalvolatile);
  • 声明顺序(与代码中定义顺序一致)。

3. 方法信息(Method 信息)

方法区存储每个方法的完整信息,包括:

  • 方法名称(如 toStringgetAge);
  • 返回类型(如 voidintString);
  • 参数列表(数量、类型及顺序);
  • 修饰符(如 publicprivatestaticfinalabstract);
  • 字节码指令、操作数栈大小、局部变量表结构(abstractnative 方法除外);
  • 异常表(记录异常处理的范围、类型及处理逻辑,abstractnative 方法除外)。

4. 静态变量(Static Variables)

静态变量属于类级别的变量(非实例变量),存储在方法区中(JDK 7 后移至堆中,但逻辑上仍与类元数据关联)。

示例

1
2
3
4
public class Test {
private static int count = 10; // 静态变量
private static final int NUMBER = 20; // 静态常量
}
  • 静态变量 count:在类加载时初始化,存储位置随 JDK 版本变化(JDK 7 前在永久代,JDK 7 后在堆);
  • 静态常量 NUMBER:编译期已确定值(ConstantValue: int 20),直接存储在常量池中。

5. 运行时常量池(Runtime Constant Pool)

运行时常量池是方法区的重要组成部分,是Class 文件常量池在内存中的映射,随类加载而创建。

  • Class 文件常量池:编译期生成,存储字面量(如字符串、整数)和符号引用(如类名、方法名的字符串标识);
  • 运行时常量池:类加载后,将 Class 文件常量池的内容加载到内存,同时支持动态性 —— 运行时可通过 String.intern() 方法新增常量(如动态生成的字符串)。

示例
Class 文件中的常量池片段(通过 javap -v 查看):

1
2
3
4
5
6
Constant pool:
#1 = Methodref #4.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#21 // com/example/Test.count:I
#3 = Class #22 // com/example/Test
#9 = Integer 20 // 静态常量NUMBER的值
#10 = Utf8 count // 静态变量count的名称

方法区的垃圾回收

方法区的垃圾回收频率远低于堆,主要回收两类内容:废弃的常量无用的类

1. 废弃常量的回收

常量池中的常量(如字符串、整数)若不再被任何地方引用,则成为 “废弃常量”,可被回收。

示例
字符串常量 "abc" 若在方法区中存在,但系统中没有任何字符串引用它,则 GC 可回收该常量。

2. 无用类的回收

类的回收条件严格,需同时满足以下三个条件:

  • 该类的所有实例已被回收(堆中无该类的任何对象);
  • 加载该类的类加载器已被回收;
  • 该类的 Class 对象无任何地方引用(无法通过反射访问该类)。

注意:即使满足上述条件,类也不一定会被回收,JVM 可根据配置决定是否回收(如 -Xnoclassgc 禁用类回收)。

方法区的参数配置

方法区的内存大小可通过参数配置,不同 JDK 版本的参数不同:

1. JDK 8 及以后(元空间)

  • -XX:MetaspaceSize:元空间初始大小(默认约 21MB),首次达到该值时触发 Full GC 并调整阈值;
  • -XX:MaxMetaspaceSize:元空间最大大小(默认无限制,可设为具体值如 256m);
  • -XX:MinMetaspaceFreeRatio:GC 后元空间最小空闲比例(默认 40%,低于该值则扩展);
  • -XX:MaxMetaspaceFreeRatio:GC 后元空间最大空闲比例(默认 70%,高于该值则收缩)。

2. JDK 7 及以前(永久代)

  • -XX:PermSize:永久代初始大小(默认约 20MB);
  • -XX:MaxPermSize:永久代最大大小(默认约 82MB)。

方法区 OOM 异常

  • JDK 7 及以前:java.lang.OutOfMemoryError: PermGen space(永久代满);
  • JDK 8 及以后:java.lang.OutOfMemoryError: Metaspace(元空间满,通常因类过多或元空间上限过小)。

欢迎关注我的其它发布渠道