Java类在JVM中的存储可分为类的元数据和实例数据两部分,分别对应不同的内存区域,且与类加载过程紧密关联。以下是具体存储方式的详细解析:
一、核心存储区域:方法区(Method Area)
类的元数据信息(描述类本身的数据,非实例数据)主要存储在方法区(JDK 8及以上称为“元空间Metaspace”,JDK 7及之前称为“永久代PermGen”)。
方法区存储的类元数据包括:
-
类的基本信息
- 类的全限定名(如
com.example.User)、父类的全限定名(判断继承关系)。 - 访问修饰符(
public/abstract/final等)。 - 实现的接口列表(如
implements Runnable)。
- 类的全限定名(如
-
字段(成员变量)信息
-
所有字段的名称、类型(基本类型/引用类型)、访问修饰符(
private/protected等)。 -
字段的静态值(
static变量,如public static int count = 0的初始值)。
注意:JDK 7后,静态变量从方法区移至堆中(作为类的Class对象的一部分)。
-
-
方法信息
- 所有方法的名称、返回值类型、参数列表(方法签名)。
- 方法的访问修饰符(
public/private等)、异常表(throws声明的异常)。 - 方法的字节码(
code属性,编译后的指令)、局部变量表大小、操作数栈大小等。
-
运行时常量池(Runtime Constant Pool)
- 类的常量池(编译期生成的字面量和符号引用)在类加载后转换而来,包括:
- 字面量(字符串、基本类型常量等,如
"hello"、123)。 - 符号引用(类/接口的全限定名、字段/方法的名称和描述符等)。
- 字面量(字符串、基本类型常量等,如
- 作用:为字节码指令提供常量解析(如
ldc指令加载常量)。
- 类的常量池(编译期生成的字面量和符号引用)在类加载后转换而来,包括:
-
其他信息
- 类的加载器引用(记录加载该类的类加载器,用于双亲委派机制)。
- 类的初始化状态(是否已完成初始化,如
<clinit>()方法是否执行)。
二、类的实例数据:堆(Heap)
当通过new关键字创建类的实例时,实例对象存储在堆内存中,包含:
- 实例变量(非静态成员变量)的值(如
User对象的name、age等)。 - 对象头(Object Header):存储对象的哈希码、GC分代年龄、锁状态等元数据(与JVM实现相关,如HotSpot的
mark word)。
示例:
User user = new User();
user(引用)存储在栈中,指向堆中的User实例对象。- 堆中的
User实例包含name、age等实例变量的值。
三、类的访问入口:Class对象
每个被加载的类,在堆内存中会生成一个对应的java.lang.Class对象,作为访问方法区中类元数据的“入口”。
- 作用:通过
Class对象可反射获取类的元数据(如user.getClass().getFields()获取字段)。 - 关系:
Class对象是类的实例(特殊实例),存储在堆中,其引用可被栈中的变量持有(如Class<?> clazz = User.class)。
四、总结:类在JVM中的存储全景
| 数据类型 | 存储区域 | 包含内容 |
|---|---|---|
| 类的元数据 | 方法区(元空间) | 类名、字段/方法信息、运行时常量池等 |
| 类的实例对象 | 堆内存 | 实例变量、对象头等 |
| 类的Class对象 | 堆内存 | 访问类元数据的入口,反射的基础 |
| 对象引用(变量) | 虚拟机栈 | 指向堆中实例对象或Class对象的地址 |
关键补充:JDK版本差异
- JDK 7及之前:方法区采用“永久代”(属于JVM内存),容易因类元数据过多导致
OutOfMemoryError: PermGen space。 - JDK 8及之后:用“元空间”替代永久代,元空间使用本地内存(不受JVM堆大小限制),降低了内存溢出风险,但仍可能因本地内存耗尽报错。
这种存储设计实现了“类的模板信息”与“实例数据”的分离,既保证了类元数据的共享(所有实例共用一份类信息),又通过堆内存灵活管理实例对象的生命周期。