在类加载的“准备”阶段,JVM 是如何给类变量分配内存和赋初始值的?
在 JVM 的类加载机制中,准备(Preparation)阶段是链接(Linking)过程的第二步。在这个阶段,JVM 主要完成两件事:为类变量分配内存和为类变量设置默认初始值。
下面我将详细拆解这两个过程的具体实现细节:
一、 内存分配:给“谁”分配?分配到“哪”?
1. 给“谁”分配?(只针对类变量)
在准备阶段,JVM 只为类变量(即被 static 修饰的变量)分配内存,而不包括实例变量。
- 类变量:跟随类的加载而分配,全类共享。
- 实例变量:不会在这个阶段分配内存,它们会在对象实例化(
new操作)时,随着对象一起分配在 Java 堆中。
2. 分配到“哪”?(随 JDK 版本的变迁而不同)
根据《Java 虚拟机规范》,这些变量所使用的内存都应当在方法区(Method Area)中进行分配。但是,在 HotSpot 虚拟机的具体实现中,这个位置发生过变化:
- JDK 6 及之前:静态变量存放在方法区的永久代(PermGen)中。
- JDK 7 开始:HotSpot 将字符串常量池和静态变量从永久代中剥离,转移到了 Java 堆(Heap)中。静态变量被存放在与该类对应的
java.lang.Class对象的末尾。 - JDK 8 及之后:永久代被废除,取而代之的是元空间(Metaspace)。虽然类的元数据(类型信息、方法信息等)存放在本地内存的元空间中,但静态变量依然存放在 Java 堆中(紧挨着 Class 对象)。
二、 赋初始值:赋什么值?
在这个阶段,JVM 给类变量赋的是数据类型的默认零值(Zero Value),而不是我们在代码中显式赋予的值。
1. 默认零值列表:
| 数据类型 | 准备阶段的初始值 |
|---|---|
int |
0 |
long |
0L |
short |
(short) 0 |
byte |
(byte) 0 |
char |
'\u0000' (空字符) |
float |
0.0f |
double |
0.0d |
boolean |
false |
引用类型 (如 String, Object) |
null |
2. 经典例子:
假设你在代码里这样写:
java
public static int value = 123;
- 在准备阶段:JVM 会为
value分配内存,并将其初始值设置为0。 - 为什么不是 123? 因为把
value赋值为123的指令(putstatic)被编译到了类的初始化方法<clinit>()中。真正的赋值动作要等到类加载的最后一个阶段——初始化(Initialization)阶段才会执行。
三、 特例:ConstantValue 属性(被 final 修饰的常量)
虽然绝大多数类变量在准备阶段被赋为零值,但有一个重要的特例:如果类变量被 final 修饰,且在编译时就能确定其值,它会在准备阶段直接被赋予代码中指定的值。
在编译时,Javac 编译器会为这种变量生成一个 ConstantValue 属性。JVM 在准备阶段如果发现该变量带有 ConstantValue 属性,就会直接按此属性进行赋值。
对比示例:
java
// 普通类变量:准备阶段赋为 0,初始化阶段赋为 123
public static int A = 123;
// 静态常量(带有 ConstantValue):准备阶段直接赋为 123
public static final int B = 123;
// 静态常量(但不带有 ConstantValue,值要在运行期确定):准备阶段赋为 null,初始化阶段才调用方法赋值
public static final String C = new String("hello");
注:只有基本数据类型和字面量形式的 String 且被 static final 修饰,才会在准备阶段直接赋值。
总结
在 JVM 的准备阶段:
- 对象是谁:仅限
static修饰的静态变量。 - 存放位置:逻辑上在方法区,HotSpot 实际实现中(JDK 7 起)存放在Java 堆的 Class 对象里。
- 赋初始值:普通静态变量赋默认零值(如 0, null, false);被
static final修饰的字面量常量,直接赋代码中指定的真实值。