Java基础:类加载的过程

1 Java中的类加载过程

在Java中,类加载是指将类的字节码文件(.class)加载到JVM内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程。这个过程由类加载器(ClassLoader)完成,是Java运行时环境的重要组成部分。以下是类加载的详细过程:

一、类加载的生命周期

类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期包括以下7个阶段:

  1. 加载(Loading)
  2. 验证(Verification)
  3. 准备(Preparation)
  4. 解析(Resolution)
  5. 初始化(Initialization)
  6. 使用(Using)
  7. 卸载(Unloading)

其中,加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,而解析阶段可能在初始化之后进行(支持动态绑定)。

二、类加载的具体过程

1. 加载(Loading)
  • 作用:通过类的全限定名(如java.lang.String)获取其二进制字节流,并将字节流所代表的静态存储结构转换为方法区的运行时数据结构,最后在内存中生成一个代表这个类的java.lang.Class对象。
  • 实现方式
    • 从本地文件系统加载.class文件;
    • 从JAR包、WAR包等归档文件中加载;
    • 通过网络(如HTTP、FTP)加载;
    • 动态生成(如使用反射、CGLIB等);
    • 从数据库或其他数据源加载。
2. 验证(Verification)
  • 作用:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
  • 验证内容
    • 文件格式验证:验证字节流是否符合Class文件格式规范(如魔数0xCAFEBABE、主次版本号等)。
    • 元数据验证:对字节码描述的信息进行语义分析(如是否继承了final类、是否实现了抽象方法等)。
    • 字节码验证:通过数据流和控制流分析,确保方法体中的指令合法(如类型转换是否安全)。
    • 符号引用验证:确保解析动作能正常执行(如符号引用中的类、字段、方法是否存在且可访问)。
3. 准备(Preparation)
  • 作用:为类变量(static修饰的变量)分配内存设置初始值(如int类型初始值为0,boolean为false,引用类型为null)。
  • 注意事项
    • 不包含实例变量:实例变量会在对象实例化时随对象一起分配在堆中。
    • 初始值为默认值:若类变量被final修饰且为常量(如static final int value = 123),则在准备阶段会直接初始化为指定值(123)。
4. 解析(Resolution)
  • 作用:将常量池中的符号引用替换为直接引用的过程。
    • 符号引用:以一组符号来描述所引用的目标(如类的全限定名、方法名和描述符)。
    • 直接引用:直接指向目标的指针、相对偏移量或句柄。
  • 解析时机
    • 静态解析:在类加载时完成(如final方法、static方法等)。
    • 动态解析:在运行时完成(如多态方法调用)。
5. 初始化(Initialization)
  • 作用:执行类构造器<clinit>()方法,为类变量赋予正确的初始值(即程序员定义的值)。
  • 触发条件
    • 遇到newgetstaticputstaticinvokestatic字节码指令(如实例化对象、访问静态字段或方法)。
    • 使用反射调用类时。
    • 初始化一个类时,若其父类尚未初始化,则先触发父类的初始化。
    • 虚拟机启动时,用户指定的主类(包含main()方法的类)。
  • <clinit>()方法
    • 由编译器自动收集类中的所有静态变量赋值语句和静态代码块合并产生。
    • 与实例构造器<init>()不同,不需要显式调用父类构造器,虚拟机会保证父类的<clinit>()先执行。

三、类加载器(ClassLoader)

类加载器负责实现“加载”阶段,将字节码文件加载到内存中。Java提供了以下几种系统级的类加载器:

  1. 启动类加载器(Bootstrap ClassLoader)
    • 由C++实现,负责加载Java核心库(如rt.jarjava.lang包等),无法被Java程序直接引用。
  2. 扩展类加载器(Extension ClassLoader)
    • sun.misc.Launcher$ExtClassLoader实现,负责加载jre/lib/ext目录下的类库。
  3. 应用程序类加载器(Application ClassLoader)
    • sun.misc.Launcher$AppClassLoader实现,负责加载用户类路径(classpath)上的类库,是ClassLoader.getSystemClassLoader()的返回值。
双亲委派模型(Parents Delegation Model)
  • 工作流程
    1. 当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是把请求委派给父类加载器去完成。
    2. 每一层的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中。
    3. 只有当父加载器反馈自己无法完成这个加载请求(搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
  • 优势
    • 保证Java核心类的安全性(如java.lang.Object始终由启动类加载器加载)。
    • 避免类的重复加载(相同全限定名的类只会被加载一次)。

四、类加载的示例与验证

以下代码演示了类加载的触发条件和初始化顺序:

public class ClassLoadingDemo {
   
   
    static {
   
   
        System.out.println("Main类初始化");
    }

    public static void main(String[] args) throws ClassNotFoundException {
   
   
        // 1. 主动使用:实例化对象
        new SubClass(); // 触发ParentClass和SubClass的初始化

        // 2. 主动使用:访问静态字段
        System.out.println(SubClass.value); // 触发ParentClass的初始化(静态字段定义在父类中)

        // 3. 被动使用:通过子类引用父类的静态字段,不会触发子类初始化
        System.out.println(SubClass.parentValue); // 仅触发ParentClass的初始化

        // 4. 被动使用:数组定义,不会触发ElementClass的初始化
        ElementClass[] array = new ElementClass[10]; // 不触发ElementClass初始化

        // 5. 被动使用:常量(static final)在编译阶段会存入调用类的常量池,不会触发定义类的初始化
        System.out.println(ConstClass.HELLO); // 不触发ConstClass初始化
    }
}

class ParentClass {
   
   
    static int value = 100;
    static int parentValue = 200;

    static {
   
   
        System.out.println("ParentClass初始化");
    }
}

class SubClass extends ParentClass {
   
   
    static {
   
   
        System.out.println("SubClass初始化");
    }
}

class ElementClass {
   
   
    static {
   
   
        System.out.println("ElementClass初始化");
    }
}

class ConstClass {
   
   
    static final String HELLO = "
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值