(12)《JVM类加载过程》

2026-03-22
9
-
- 分钟
|

核心思想:类加载是JVM“认识”类的过程,类似“建房子”——先画图纸(加载),再检查图纸(链接),最后装修(初始化)。全程懒加载(用到才触发),避免浪费资源。


类加载过程

1. 加载(Loading)

通俗解释:JVM把.class文件(字节码)从磁盘读入内存,存到元空间(JDK8+取代了“永久代”,在本地内存,不是堆)。
JVM用C++的instanceKlass表示类(类的“骨架”)。
instanceKlass的重要field:

  • 关键结构
    • _Java_mirror:类的“Java身份证”(桥梁),指向堆中的Class对象(Java代码能直接用的类实例)。
  • 其他字段:
    - _super → 父类
    - _fields → 成员变量
    - _methods → 方法列表
    - _constants → 常量池(符号引用)
    - _class_loader → 类加载器(谁加载的)
    - _vtable/_itable → 虚方法表/接口方法表(多态用的)

加载流程

  1. 先加载父类(如果未加载)→ 避免“没地基就盖楼”。

  2. 在元空间:创建instanceKlass(含_Java_mirror指向堆)。

  3. 在堆中:创建Class对象(Java代码用的类实例)。

    💡 比喻:加载是“把图纸(字节码)存进仓库(元空间)”,_Java_mirror是“仓库门牌号”,指向“类的身份证(Class对象)”。

类调用示例

  • 当调用MyClass.staticMethod()
    1. JVM通过Class对象(堆中)→ 找到_Java_mirror → 定位元空间的instanceKlass → 执行方法。
      对象头存的是对象实例地址,类的访问依赖Class对象(堆中)。

2. 链接(Linking)

通俗解释:JVM“检查图纸+规划装修”,分三步,可能和加载交替进行(优化性能)。

  • 验证(Verification)
    • 检查字节码是否合法(如指令是否正确、类型是否匹配),防止恶意代码。

      💡 例:确保int x = "abc";这种错误被拦截。

  • 准备(Preparation)
    • static变量分配内存,设默认值(如int→0,String→null)。
    • 关键区别
      • final static变量:在准备阶段赋值(编译时确定,如final int a = 10)。

      • final变量:准备阶段只设默认值,初始化阶段赋实际值

      💡 为什么?避免在准备阶段执行代码(可能出错)。

  • 解析(Resolution)
    • 把常量池里的符号引用(如"java/lang/String")→ 直接引用(实际内存地址)。

      💡 例:String s = new String(); → 常量池的"java/lang/String"被解析为内存中的String类地址。


3. 初始化(Initialization)

通俗解释:JVM“装修房子”,执行静态代码块(<cinit>()V),懒加载(首次用才触发)。

  • 触发时机(必须用到类时):
    • 主类(main方法所在类)→ 总是最先初始化
    • 首次访问final static 变量/方法(如MyClass.count)。
    • 子类初始化时,父类未初始化 → 先初始化父类
    • 子类访问父类的static变量 → 只触发父类初始化
    • Class.forName("MyClass")(默认自动初始化)。
    • new MyClass()触发初始化
  • 不触发初始化的情况(安全懒加载):
    • 访问static final基本类型或字符串,如public static final int A = 10)。
    • MyClass.class → 只获取Class对象,不触发初始化。
    • new MyClass[10](创建数组,不涉及类逻辑)。
    • ClassLoader.loadClass("MyClass") → 仅加载,不链接/初始化。
    • Class.forName("MyClass", false, loader) → 第二个参数false表示不初始化。

💡 为什么static final不触发?
因为值在编译时确定(如10"Hello"),JVM直接用常量,无需执行初始化代码。


案例:懒加载单例模式(完美利用初始化特性)

public class LazySingleton {
    private LazySingleton() {} // 私有构造器

    // 静态内部类(懒加载核心!)
    private static class Holder {
        public static final LazySingleton INSTANCE = new LazySingleton();
    }

    public static LazySingleton getInstance() {
        return Holder.INSTANCE; // 首次调用时才初始化
    }
}

为什么安全?

  • Holder仅在getInstance()首次调用时加载 → 触发初始化 → 创建INSTANCE
  • INSTANCEstatic final编译时确定值,无需额外同步(JVM保证线程安全)。
  • 对比饿汉式new LazySingleton()在类加载时就执行,浪费资源。

补充:此模式完美利用了初始化懒加载和**static final不触发初始化**的特性。


关键总结(一图读懂)

阶段作用重要细节懒加载?
加载读入字节码 → 元空间_Java_mirror连接C++和Java
链接检查+准备+解析static准备阶段设默认值,final提前赋值
初始化执行<cinit>()Vfinal static访问才触发

💡 JVM底层小贴士:元空间(Metaspace)在本地内存,避免OOM(JDK8+改进),而堆内存是Java对象的“家”。类加载是JVM安全的起点,后续所有操作都依赖它!

为什么这么设计类加载

核心原因:安全、效率与灵活性的平衡


1. 沙箱安全机制(为什么需要双亲委派)

通俗解释:JVM像一个"严格安检站",防止你用自定义的String类替换Java核心类。

  • 问题:如果允许你直接定义java.lang.String,那么所有程序都会使用你的String,破坏Java核心功能。
  • 解决方案:双亲委派机制确保所有类加载请求先交给父加载器(尤其是启动类加载器)。
    • 例如:你试图加载java.lang.String,JVM会先让启动类加载器(C++实现)去加载rt.jar中的原版String不会让你的类加载器加载
  • 结果:形成"沙箱",保护Java核心API不被篡改。

💡 类比:就像银行ATM机,你不能用自己的银行卡替换银行的系统卡,必须通过银行的总系统验证。


2. 内存管理优化(为什么分元空间和堆)

通俗解释:元空间(本地内存)存"类的骨架",堆中存"类的身份证",各司其职。

位置存储内容作用为什么这样设计?
元空间instanceKlass(C++结构)类的"骨架"(方法表、常量池等)本地内存,避免OOM(JDK8+取代永久代)
堆中Class对象(Java结构)类的"身份证"(Java代码能直接用)堆内存是Java对象的"家",方便Java代码访问
  • 关键设计_Java_mirror是桥梁,连接C++(元空间)和Java(堆)。
    • 元空间存储JVM内部结构instanceKlass)。
    • 堆中存储Java可操作的类对象Class)。
  • 结果:既保证JVM高效运行,又让Java代码能直接使用类。

💡 类比:元空间是"建筑图纸"(C++),堆中是"房屋实体"(Java对象),_Java_mirror是"门牌号",帮你从图纸找到房子。


3. 类加载的懒加载与性能

通俗解释:类不是一次性全部加载,而是"用到才加载",避免浪费资源。

  • 为什么需要懒加载
    • 一个大型应用可能有1000+类,但只有20%的类在启动时用到
    • 如果全部加载,会浪费CPU和内存。
  • JVM如何实现
    • 类加载是懒触发的(如main方法所在类最先加载,其他类用到才加载)。
    • 双亲委派机制确保类只加载一次(避免重复加载)。

💡 类比:就像你家的冰箱,不是一次性把所有食物都放进去,而是"需要时才买",避免浪费空间。


4. 为什么需要"先加载父类"?

通俗解释:类是"搭积木",必须先有地基(父类)才能盖楼(子类)。

  • 问题:如果子类先加载,但父类没加载,子类无法知道父类的结构。
  • 解决方案先加载父类,确保子类能正确引用父类。
    • 例如:class Child extends Parent,JVM会先加载Parent,再加载Child

💡 类比:盖房子要先打地基(父类),再搭主体(子类),不能先盖屋顶。


总结:为什么这样设计?

设计点原因结果
双亲委派防止核心类被篡改(沙箱安全)确保Java核心API的完整性
元空间+堆分离本地内存存JVM结构,堆存Java对象避免OOM,提高内存管理效率
_Java_mirror桥接C++(元空间)和Java(堆)使Java代码能高效访问JVM内部数据
懒加载避免一次性加载所有类减少启动时间,节省资源
先加载父类确保子类能正确引用父类防止类继承链断裂

一句话总结:JVM的类加载设计,是为了解决安全(沙箱)、效率(懒加载)、一致性(双亲委派)三大核心问题,让Java既安全又灵活。

评论交流

文章目录