JVM 启动到内存分配的完整顺序
一、JVM 启动到内存分配的完整顺序(结合内存区域)
┌─────────────────────────────────────────────────────────────┐ │ JVM 启动完整流程 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 1. 分配JVM进程内存 │ │ ↓ │ │ 2. 初始化运行时数据区(按固定顺序) │ │ ├─ a. PC寄存器(线程创建时分配) │ │ ├─ b. 方法区/元空间(JVM启动时) │ │ ├─ c. 堆内存(JVM启动时) │ │ ├─ d. 栈内存(线程创建时) │ │ └─ e. 本地方法栈(线程创建时) │ │ ↓ │ │ 3. 类加载过程(按需触发) │ │ ├─ a. 加载(Loading)→ 方法区/元空间 │ │ ├─ b. 验证(Verification) │ │ ├─ c. 准备(Preparation)→ 方法区/元空间 │ │ ├─ d. 解析(Resolution) │ │ └─ e. 初始化(Initialization) │ │ ↓ │ │ 4. 创建对象实例 → 堆内存 │ │ ↓ │ │ 5. 执行方法 → 栈内存 │ └─────────────────────────────────────────────────────────────┘
二、详细顺序与内存区域对应关系
阶段1:JVM 启动时的内存区域初始化顺序
public class JVMBootSequence {
/**
* 顺序1:JVM进程启动,从操作系统申请内存
* 操作系统 → JVM进程内存空间
*/
/**
* 顺序2:初始化方法区/元空间(最先)
* 大小由-XX:PermSize/-XX:MetaspaceSize控制
* 存储:类信息模板
*/
/**
* 顺序3:初始化堆内存(次之)
* 大小由-Xms/-Xmx控制
* 此时还是空的,没有对象
* ├─ 年轻代(Young Generation)
* ├─ 老年代(Old Generation)
* └─ (JDK8+)字符串常量池也在堆中初始化
*/
/**
* 顺序4:创建主线程,初始化线程私有区域(最后)
* ├─ PC寄存器(Program Counter)
* ├─ 栈(Stack)- 由-Xss控制大小
* └─ 本地方法栈(Native Method Stack)
*/
}
阶段2:类加载过程的内存分配顺序(核心!)
public class ClassLoadingSequence {
// ========== 第一步:触发类加载时机 ==========
// 1. new关键字(最常见)
// 2. 访问类的静态成员
// 3. 反射调用
// 4. 子类被加载,父类先加载
// 5. 主类(包含main方法)
// ========== 第二步:加载过程的内存分配 ==========
/**
* 1. 加载(Loading)阶段
* 内存分配:方法区/元空间
* 操作:
* - 找到.class文件
* - 读取二进制流
* - 在方法区/元空间创建Class对象的结构
* - 在堆中创建java.lang.Class实例(作为访问入口)
*/
/**
* 2. 验证(Verification)阶段
* 内存:主要在栈中进行验证操作
* 操作:验证字节码合法性
*/
/**
* 3. 准备(Preparation)阶段 - 关键!
* 内存分配:根据JDK版本不同
*
* JDK7及以前(方法区/永久代):
* - 静态变量 → 方法区
* - 常量 → 运行时常量池(方法区内)
*
* JDK8+(元空间 + 堆):
* - 静态变量 → 堆
* - 常量 → 堆中的运行时常量池
* - 类信息(Klass) → 元空间
*
* 注意:此时只赋"零值",不是程序中的初始值!
*/
private static int staticInt; // 准备阶段:值 = 0
private static String staticString; // 准备阶段:值 = null
private static final int CONSTANT = 100; // 准备阶段:值 = 100(常量特殊)
/**
* 4. 解析(Resolution)阶段
* 内存:方法区/元空间
* 操作:将符号引用转为直接引用
*/
/**
* 5. 初始化(Initialization)阶段 - 执行代码!
* 内存:栈(执行代码)+ 堆(赋真实值)
* 操作:
* - 执行静态代码块
* - 为静态变量赋真实值
*/
static {
// 在栈中执行,为堆中的staticInt赋值
staticInt = 10; // 初始化阶段:值 = 10
staticString = "init"; // 字符串对象在堆中创建
System.out.println("静态代码块执行");
}
}
三、对象创建的内存分配顺序
public class ObjectCreationSequence {
// 类加载完成后,可以创建对象实例
// 实例变量
private int instanceVar = 5; // 初始化值
private String instanceStr = "hello"; // 引用类型
private final int finalVar = 20; // 常量
// 实例代码块
{
System.out.println("实例代码块执行,instanceVar=" + instanceVar);
instanceVar = 10; // 修改初始值
}
// 构造方法
public ObjectCreationSequence() {
System.out.println("构造方法执行,instanceVar=" + instanceVar);
instanceVar = 15; // 再次修改
}
public static void main(String[] args) {
/**
* 创建对象的内存分配顺序:
*
* 1. new 指令触发
* ↓
* 2. 检查类是否已加载
* ├─ 未加载 → 先执行类加载过程
* └─ 已加载 → 继续
* ↓
* 3. 在堆中分配对象内存
* ├─ 计算对象大小(包含所有实例变量)
* ├─ 在年轻代Eden区分配空间
* └─ 内存初始化为零值
* ↓
* 4. 设置对象头信息
* ├─ Mark Word(哈希码、GC年龄、锁状态等)
* ├─ 类型指针(指向方法区/元空间的类信息)
* └─ (如果是数组)数组长度
* ↓
* 5. 执行实例初始化(按顺序!)
* ├─ 先:实例变量直接初始化(instanceVar=5)
* ├─ 再:实例代码块执行({...})
* └─ 最后:构造方法执行(ObjectCreationSequence())
* ↓
* 6. 将对象引用返回给栈中的局部变量
*/
// 触发对象创建
ObjectCreationSequence obj = new ObjectCreationSequence();
// 输出最终结果
System.out.println("最终 instanceVar=" + obj.instanceVar); // 15
}
}
四、方法执行时的内存分配顺序
public class MethodExecutionSequence {
private static int staticCounter = 0;
private int instanceCounter = 0;
public void executeMethod(int param1, String param2) {
/**
* 方法调用时的内存分配顺序:
*
* 1. 调用方法前:
* - 参数值已计算完成(在调用者栈帧中)
*
* 2. 创建新的栈帧:
* ├─ 操作数栈(Operand Stack)
* ├─ 局部变量表(Local Variables)
* └─ 动态链接、返回地址等
*
* 3. 参数传递到新栈帧:
* - param1 → 局部变量表 slot 0
* - param2 → 局部变量表 slot 1
* - this引用(非静态方法)→ slot 2
*
* 4. 执行方法体:
* - 局部变量分配在栈中
* - new对象分配在堆中
* - 静态变量访问:通过方法区/元空间找到堆中位置
* - 实例变量访问:通过this引用找到堆中对象
*/
// 局部基本类型 → 栈
int localInt = 100;
// 局部引用类型 → 栈(引用),对象在堆
String localStr = new String("local");
// 访问静态变量 → 通过方法区/元空间找到堆中位置
staticCounter++;
// 访问实例变量 → 通过this找到堆中对象
instanceCounter++;
// 方法调用 → 创建新的栈帧
helperMethod(localInt);
}
private void helperMethod(int value) {
// 递归调用会不断创建栈帧
if (value > 0) {
helperMethod(value - 1);
}
}
public static void main(String[] args) {
/**
* main方法执行顺序:
* 1. JVM创建主线程
* 2. 为main方法创建栈帧
* 3. args参数入栈
* 4. 执行main方法体
*/
MethodExecutionSequence obj = new MethodExecutionSequence();
obj.executeMethod(10, "test");
// 可能发生栈溢出
// obj.helperMethod(10000); // StackOverflowError
}
}
五、综合示例:完整的内存分配顺序
public class CompleteMemorySequence {
// ========== 类级别(方法区/元空间) ==========
// 1. 类信息加载到方法区/元空间
// 2. 常量池信息加载
// ========== 静态成员(准备阶段) ==========
private static int staticInt; // 准备:0 → 堆(JDK8+)
private static Object staticObj; // 准备:null → 堆(JDK8+)
private static final String CONSTANT = "CONST"; // 准备:"CONST" → 常量池
// ========== 静态代码块(初始化阶段) ==========
static {
// 执行顺序:在栈中执行,为堆中的静态变量赋值
staticInt = 100;
staticObj = new Object(); // 在堆中创建Object对象
System.out.println("静态代码块:staticInt=" + staticInt);
}
// ========== 实例成员(对象创建时) ==========
private int instanceInt = 50; // 对象创建时初始化
private String instanceStr; // 默认null
private final int finalInt = 200; // 常量
// ========== 实例代码块(对象创建时) ==========
{
// 在构造方法之前执行
instanceStr = "initialized";
System.out.println("实例代码块:instanceInt=" + instanceInt);
}
// ========== 构造方法 ==========
public CompleteMemorySequence() {
// 最后执行
instanceInt = 300;
System.out.println("构造方法:instanceInt=" + instanceInt);
}
// ========== 方法执行 ==========
public void process() {
// 方法调用时创建栈帧
int localVar = 500; // 栈中分配
Object localObj = new Object(); // 堆中创建,引用在栈
// 访问不同内存区域的数据
System.out.println("静态变量访问:" + staticInt); // 通过元空间→堆
System.out.println("实例变量访问:" + instanceInt); // 通过this→堆
System.out.println("局部变量访问:" + localVar); // 直接栈中访问
}
// ========== main方法 ==========
public static void main(String[] args) {
/**
* 完整执行顺序:
*
* 阶段1:类加载(第一次使用时)
* 1. 加载CompleteMemorySequence类到方法区/元空间
* 2. 准备阶段:为静态变量分配内存(堆/方法区)
* 3. 初始化阶段:执行静态代码块
*
* 阶段2:main方法执行
* 1. 为主线程创建栈
* 2. 为main方法创建栈帧
* 3. args参数入栈
*
* 阶段3:创建对象
* 1. new指令触发
* 2. 堆中分配对象内存
* 3. 实例变量赋零值
* 4. 设置对象头
* 5. 执行实例代码块
* 6. 执行构造方法
* 7. 引用返回到栈中
*
* 阶段4:方法调用
* 1. 为process方法创建新栈帧
* 2. 参数、局部变量在栈中分配
* 3. new对象在堆中分配
*/
System.out.println("=== 开始执行 ===");
// 第一次使用类,触发类加载
System.out.println("CONSTANT=" + CONSTANT);
// 创建对象
CompleteMemorySequence obj = new CompleteMemorySequence();
// 调用方法
obj.process();
System.out.println("=== 执行结束 ===");
}
}
六、内存分配顺序总结表
| 阶段 | 触发条件 | 主要内存区域 | 操作顺序 |
|---|---|---|---|
| JVM启动 | 启动java命令 | 所有区域 | 元空间 → 堆 → 栈 |
| 类加载 | 第一次使用类 | 方法区/元空间 + 堆 | 加载 → 验证 → 准备 → 解析 → 初始化 |
| 静态初始化 | 类初始化阶段 | 栈(执行)+ 堆(存储) | 父类静态 → 子类静态 |
| 对象创建 | new关键字 | 堆(对象)+ 栈(引用) | 分配内存 → 设零值 → 实例代码块 → 构造方法 |
| 方法调用 | 方法调用 | 栈(栈帧) | 创建栈帧 → 参数入栈 → 局部变量分配 |
| 数组创建 | new数组 | 堆 | 分配连续内存 → 设零值 |
| 字符串创建 | 字面量/new | 堆(常量池/对象) | 常量池查找 → 不存在则创建 |
七、验证代码:观察加载顺序
public class LoadingOrderValidator {
static class Parent {
static String parentStatic = "父类静态变量";
static {
System.out.println("父类静态代码块:" + parentStatic);
}
String parentInstance = "父类实例变量";
{
System.out.println("父类实例代码块:" + parentInstance);
}
Parent() {
System.out.println("父类构造方法");
}
}
static class Child extends Parent {
static String childStatic = "子类静态变量";
static {
System.out.println("子类静态代码块:" + childStatic);
}
String childInstance = "子类实例变量";
{
System.out.println("子类实例代码块:" + childInstance);
}
Child() {
System.out.println("子类构造方法");
}
}
public static void main(String[] args) {
System.out.println("=== 第一次使用Child类,触发类加载 ===");
// 访问静态变量,触发类加载但不创建对象
System.out.println("访问静态变量:" + Child.childStatic);
System.out.println("\n=== 创建Child对象 ===");
Child child = new Child();
System.out.println("\n=== 再次访问静态变量(类已加载) ===");
System.out.println("再次访问:" + Child.childStatic);
}
}
输出结果:
=== 第一次使用Child类,触发类加载 === 父类静态代码块:父类静态变量 子类静态代码块:子类静态变量 访问静态变量:子类静态变量 === 创建Child对象 === 父类实例代码块:父类实例变量 父类构造方法 子类实例代码块:子类实例变量 子类构造方法 === 再次访问静态变量(类已加载) === 再次访问:子类静态变量
八、关键结论
-
内存区域初始化顺序是固定的:元空间 → 堆 → 栈
-
类加载是"懒加载":第一次使用时才加载
-
静态先于实例:静态初始化在类加载时完成,实例初始化在对象创建时
-
父类先于子类:继承关系中父类先初始化
-
代码执行顺序:静态代码块 → 实例代码块 → 构造方法
-
JVM 版本差异:JDK8+ 静态变量在堆中,JDK7 在方法区
九、关键总结:静态方法与内存分配
1. 时间顺序
1. JVM启动 → 初始化内存区域(元空间、堆、栈) 2. 类第一次使用 → 类加载到元空间 3. 静态变量 → 准备阶段分配内存(堆/方法区) 4. 静态代码块 → 初始化阶段执行(栈中执行) 5. 静态方法调用 → 创建栈帧,执行字节码 6. 对象创建 → 堆中分配,执行构造代码 7. 实例方法调用 → 有this引用的栈帧
2. 空间分配
// 元空间:类信息 + 方法字节码(一次加载)
// 堆:静态变量值 + 对象实例(运行时分配)
// 栈:方法栈帧 + 局部变量(调用时分配)
// 常量池:字面量 + 符号引用(类加载时)
3. 静态方法特点
-
字节码位置固定:始终在元空间
-
执行位置灵活:在任何线程栈中执行
-
无状态性:没有this,不依赖对象实例
-
共享性:所有线程共享同一份字节码
-
同步单位:类锁(Class对象)
4. 性能影响
-
启动时:类加载有开销,静态初始化一次
-
运行时:静态方法调用与实例方法开销相似
-
内存:静态变量常驻内存,可能造成内存泄漏
-
并发:静态方法需注意线程安全问题
浙公网安备 33010602011771号