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对象 ===
父类实例代码块:父类实例变量
父类构造方法
子类实例代码块:子类实例变量
子类构造方法

=== 再次访问静态变量(类已加载) ===
再次访问:子类静态变量

八、关键结论

  1. 内存区域初始化顺序是固定的:元空间 → 堆 → 栈

  2. 类加载是"懒加载":第一次使用时才加载

  3. 静态先于实例:静态初始化在类加载时完成,实例初始化在对象创建时

  4. 父类先于子类:继承关系中父类先初始化

  5. 代码执行顺序:静态代码块 → 实例代码块 → 构造方法

  6. JVM 版本差异:JDK8+ 静态变量在堆中,JDK7 在方法区

九、关键总结:静态方法与内存分配

1. 时间顺序

1. JVM启动 → 初始化内存区域(元空间、堆、栈)
2. 类第一次使用 → 类加载到元空间
3. 静态变量 → 准备阶段分配内存(堆/方法区)
4. 静态代码块 → 初始化阶段执行(栈中执行)
5. 静态方法调用 → 创建栈帧,执行字节码
6. 对象创建 → 堆中分配,执行构造代码
7. 实例方法调用 → 有this引用的栈帧

2. 空间分配

// 元空间:类信息 + 方法字节码(一次加载)
// 堆:静态变量值 + 对象实例(运行时分配)
// 栈:方法栈帧 + 局部变量(调用时分配)
// 常量池:字面量 + 符号引用(类加载时)

3. 静态方法特点

  • 字节码位置固定:始终在元空间

  • 执行位置灵活:在任何线程栈中执行

  • 无状态性:没有this,不依赖对象实例

  • 共享性:所有线程共享同一份字节码

  • 同步单位:类锁(Class对象)

4. 性能影响

  • 启动时:类加载有开销,静态初始化一次

  • 运行时:静态方法调用与实例方法开销相似

  • 内存:静态变量常驻内存,可能造成内存泄漏

  • 并发:静态方法需注意线程安全问题

posted @ 2026-01-09 16:25  a快乐码农  阅读(36)  评论(0)    收藏  举报