02.深入解密Java类加载机制——从加载到初始化的完整流程

图片[1]-02.深入解密Java类加载机制——从加载到初始化的完整流程-速优课

本文原创,转载请注明出处。关注博主,持续分享Java进阶知识。

一、开篇:类加载与盖房子的奇妙类比

你知道吗?Java虚拟机加载类的过程,和盖房子有着惊人的相似之处!

盖房子流程:

找建筑师设计方案 → 市政部门审核 → 盖毛坯房 → 装修 → 入住

类加载流程:

加载(查找字节流) → 链接(验证/准备/解析) → 初始化 → 可执行

从class文件到内存中的类,需要经过加载、链接、初始化三大步骤。那么,每个步骤具体都在做什么?是否所有Java类都需要经过这几步呢?

让我们一起揭开类加载的神秘面纱!


二、Java类型体系概览

2.1 类型分类

Java语言的类型分为两大类:

类型分类具体类型是否需要加载
基本类型byte/short/char/int/long
/float/double/boolean
JVM预先定义,无需加载
引用类型类、接口需要加载
数组类JVM直接生成
泛型参数编译期擦除,不存在于JVM中

2.2 字节流的来源

类的字节流可以从多种来源获取:

图片[2]-02.深入解密Java类加载机制——从加载到初始化的完整流程-速优课

三、加载阶段:寻找字节流并创建类

3.1 类加载器的层次结构

类加载器采用双亲委派模型,等级森严:

图片[3]-02.深入解密Java类加载机制——从加载到初始化的完整流程-速优课

3.2 双亲委派模型详解

工作流程:

  1. 收到加载请求
  2. 先转发给父类加载器
  3. 父类加载器无法加载时,自己尝试加载

核心代码:

protected Class<?> loadClass(String name, boolean resolve) 
    throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 1. 检查是否已加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 2. 委托父类加载器
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 3. 父类为null时,使用启动类加载器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父类加载器抛出异常
            }
            
            if (c == null) {
                // 4. 父类无法加载,自己查找
                long t1 = System.nanoTime();
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

3.3 各加载器的职责

类加载器负责加载路径来源
启动类加载器JRE核心类(java.lang.*等)JRE/lib目录
平台类加载器Java SE扩展模块JRE/lib/ext目录
应用类加载器用户应用程序类-cp/-classpath指定
自定义类加载器按需加载自定义路径

3.4 类的唯一性

类的唯一标识 = 类加载器实例 + 类的全名

ClassLoader cl1 = new CustomClassLoader();
ClassLoader cl2 = new CustomClassLoader();
​
Class<?> c1 = cl1.loadClass("com.example.MyClass");
Class<?> c2 = cl2.loadClass("com.example.MyClass");
​
System.out.println(c1 == c2);  // false!两个不同的类

这一特性被广泛应用于:

  • 热部署/热更新
  • 运行同一类的不同版本
  • OSGi模块化框架

四、链接阶段:整合到JVM中

链接分为三个阶段:验证、准备、解析

4.1 验证阶段:确保合规性

验证内容:

  • 文件格式验证(魔数、版本号等)
  • 元数据验证(类结构、继承关系等)
  • 字节码验证(指令合法性、类型安全等)
  • 符号引用验证(引用目标存在性等)

类比: 盖房子前的市政审核

4.2 准备阶段:分配内存

主要任务:

  1. 为静态字段分配内存
  2. 设置默认初始值(0/false/null)
  3. 构造方法表等数据结构

注意: 此时尚未执行Java代码中的初始化赋值!

public class Example {
    static int a = 10;  // 准备阶段:a = 0(默认值)
    static String b;     // 准备阶段:b = null
}

4.3 解析阶段:符号引用转实际引用

符号引用 vs 实际引用:

类型说明示例
符号引用字符串描述,编译期生成“java.lang.String”
实际引用内存地址或偏移量,运行期解析指向String类的指针

解析时机: JVM规范不要求在链接阶段完成解析,只需在执行相关字节码前完成即可。


五、初始化阶段:赋予生命

5.1 初始化的内容

两种初始化方式:

public class InitializationDemo {
    // 方式1:声明时直接赋值
    static int a = 10;
    
    // 方式2:静态代码块
    static {
        a = 20;
        System.out.println("Static block");
    }
    
    // final + 基本类型/字符串:编译期常量
    static final int CONST = 100;  // 直接存入常量池
}

特殊处理:

  • final + 基本类型/字符串:编译期标记为ConstantValue,由JVM直接赋值
  • 其他静态字段赋值和静态代码块:合并到方法中

5.2 初始化触发时机

JVM规范枚举了8种触发情况:

序号触发场景示例
1启动时初始化主类java Main
2新建类实例new MyClass()
3调用静态方法MyClass.staticMethod()
4访问静态字段MyClass.staticField
5子类初始化触发父类new Child() → 先初始化Parent
6接口default方法触发实现类初始化触发接口
7反射调用Class.forName(“MyClass”)
8调用MethodHandle首次调用时初始化

5.3 经典应用:延迟初始化单例

public class Singleton {
    private Singleton() {}
    
    // 静态内部类
    private static class LazyHolder {
        static final Singleton INSTANCE = new Singleton();
        static {
            System.out.println("LazyHolder.<clinit>");
        }
    }
    
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;  // 触发LazyHolder初始化
    }
}

原理:

  • LazyHolder类在首次访问时才会被初始化
  • 类初始化是线程安全的(JVM保证方法只执行一次)
  • 实现了真正的延迟加载

六、动手实践:验证类加载过程

6.1 实验代码

public class Singleton {
    private Singleton() {}
    
    private static class LazyHolder {
        static final Singleton INSTANCE = new Singleton();
        static {
            System.out.println("LazyHolder.<clinit>");
        }
    }
    
    public static Object getInstance(boolean flag) {
        if (flag) return new LazyHolder[2];  // 新建数组
        return LazyHolder.INSTANCE;           // 访问静态字段
    }
    
    public static void main(String[] args) {
        getInstance(true);   // 新建LazyHolder数组
        System.out.println("----");
        getInstance(false);  // 访问LazyHolder.INSTANCE
    }
}

6.2 运行命令

# 编译
$ javac Singleton.java

# 运行并打印类加载信息
$ java -verbose:class Singleton

6.3 思考问题

  1. 新建数组会导致LazyHolder的加载吗?
  2. 新建数组会导致LazyHolder的初始化吗?
  3. 为什么访问LazyHolder.INSTANCE才会触发初始化?

提示: JVM规范中,创建数组类并不会触发其元素类型的初始化!


七、总结

核心要点回顾

  1. 三大步骤:加载 → 链接(验证/准备/解析) → 初始化
  2. 类加载器:双亲委派模型,启动类加载器→平台类加载器→应用类加载器→自定义类加载器
  3. 类唯一性:类加载器实例 + 类全名共同确定
  4. 初始化时机:8种触发情况,类初始化线程安全
  5. 延迟初始化:利用静态内部类实现高效线程安全的单例

关键理解

  • 基本类型由JVM预先定义,无需加载
  • 数组类由JVM直接生成,无需加载字节流
  • 解析阶段可以延迟到执行相关字节码前
  • final+基本类型/字符串的静态字段在编译期就已确定

写在最后

类加载机制是JVM的核心功能之一,深入理解它不仅能帮助我们写出更高效的代码,还能为学习反射、动态代理、热部署等高级特性打下坚实基础。

如果觉得本文对你有帮助,欢迎点赞、收藏、关注!后续我会继续分享更多JVM相关的深度解析文章。

📚 关注我,一起深入探索Java虚拟机的奥秘!

© 版权声明
THE END
喜欢就支持一下吧
点赞12
相关推荐
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容

温馨提示:
1、本内容转载于网络,版权归原作者所有!
2、本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
3、本内容若侵犯到你的版权利益,请联系我们,会尽快给予删除处理!