目录
什么是类加载机制?
类加载顺序
类加载顺序图
双亲委派模型
双亲委派模型示意图
如何打破双亲委派模型?
要想学好java,首先得知道它是什么,怎么运行的,怎么加载的,运行的是个什么东西,今天我写篇文章说一下,表演开始喽😄
Java是一门面向对象的编程语言,它的特点之一就是可以跨平台运行。为什么可以跨平台,因为Java程序不是直接编译成机器码,而是编译成一种中间格式的字节码(bytecode),然后由Java虚拟机(JVM)在不同的平台上解释执行。
那Java虚拟机是如何加载和执行字节码的?这就涉及到了Java类加载机制
什么是类加载机制?
类加载机制是Java虚拟机将字节码转换成可运行的类的过程。这个过程包括三个主要步骤:加载、链接和初始化编辑
- 加载:就是将字节码文件从不同的来源(如本地文件系统、网络、内存等等)读取到虚拟机中,并创建一个对应的Class对象,用来表示这个类在内存中的数据结构。
- 连接:就是将加载后的Class对象进行验证、准备和解析三个阶段的处理,以保证类的正确性和完整性。其中包含了下面三个小步骤
- 验证:就是检查元数据Class对象是否符合Java虚拟机规范。验证文件格式验证;验证字节码验证(确定程序语义合法,符合逻辑) ;验证符号引用验证(确保下一步的解析能正常执行)
- 准备:就是为类中的静态变量分配内存,并赋予默认值。
- 注意这时内存分配仅包括类变量static,不包括实例变量,实例变量会在对象实例化时随着对象一块分配在java堆中设置的是默认值,注意是默认值,比如static int=11,此时初始值是0,11是初始化才会赋值
- 解析:就是将类中的符号引用替换为直接引用,即确定类中各个字段、方法、接口等的实际地址。
- 初始化:就是执行类中的静态初始化器和静态初始化程序,执行静态初始化程序,把静态变量初始化成指定的值;clinit()方法由编译器自动产生,收集类中static{}代码块中类变量赋值语句和类中静态成员变量的赋值语句。此时将会执行静态代码块和静态方法。
初始化过程的注意点
- clinit()方法中静态成员变量的赋值顺序是根据Java代码中静态成员变量的出现的顺序决定的。
- 静态代码块能访问出现在静态代码块之前的静态成员变量,无法访问出现在静态代码块之后的成员变量。
- 静态代码块能给出现在静态代码块之后的静态成员变量赋值。
- 如果一个类/接口中没有静态代码块,也没有静态成员变量的赋值操作,那么编译器就不会生成clinit()方法
- 接口中不能使用静态代码块
- 接口在执行clinit()方法前,虚拟机不会确保其父接口的clinit()方法被执行,只有当父接口中的静态成员变量被使用到时才会执行父接口的clinit()方法
- 虚拟机会给clinit()方法加锁,因此当多条线程同时执行某一个类的clinit()方法时,只有一个方法会被执行,其他的方法都被阻塞。并且,只要有一个clinit()方法执行完,其它的clinit()方法就不会再被执行。因此,在同一个类加载器下,同一个类只会被初始化一次。
- 非静态成员变量只有在实例化对象的时候才会分配内存并赋值,非静态成员变量随对象一起保存在堆中
下面的实例解释下,b可以赋值,c不可以,提示Illegal forward reference,因为c在代码块下面,也就是之后
类加载顺序
一般来说,类加载顺序遵循以下原则:
主动引用:当一个类被主动引用时,该类才会被加载。主动引用包括下面几种情况:
- 创建类的实例,如new A()。
- 调用类的静态方法,如A.method()。
- 访问类或接口的静态变量,或者对该静态变量赋值,如A.field或A.field = value。
- 反射调用类的方法或构造器,如Class.forName("A")或A.class.getDeclaredMethod("method")。
- 初始化一个类的子类,如new B(),其中B是A的子类,这时候会先加载A。
- 虚拟机启动时被标明为启动类的类,如java HelloWorld。
被动引用:当一个类被被动引用时,该类不会被加载。被动引用包括下面几种情况:
- 访问或设置一个数组类型的静态变量,如A[] arr或A[].length。
- 引用一个常量字段,如A.CONSTANT,其中CONSTANT是用final修饰的静态变量,并且在编译期已经确定了值。
- 引用一个接口中定义的常量字段,如I.CONSTANT,其中CONSTANT是用public static final修饰的变量,并且在编译期已经确定了值。
- 引用一个父类中定义的静态字段,如B.field,其中field是在A中定义的静态变量,而B是A的子类。
类加载顺序图
大家可以验证下,可能需要的示例太多这里先不举例了
双亲委派模型
双亲委派模型是Java类加载机制的一个重要特征,它决定了一个类由哪个类加载器(classLoader)来加载。类加载器是Java虚拟机的一个组件,它负责根据不同的策略来加载类。Java虚拟机提供了三种内置的类加载器:
- 启动类加载器:它是最顶层的类加载器,负责加载Java核心类库,如java.lang.*、java.util.*等,以及一些虚拟机相关的类,如sun.misc.*等。它不是一个Java类,而是由C++实现的一个本地方法。
- 扩展类加载器:它是启动类加载器的子类加载器,负责加载Java扩展类库,如javax.*等,以及一些第三方提供的扩展包,如JDBC驱动等。它是一个Java类,叫做sun.misc.Launcher$ExtClassLoader。
- 应用类加载器:它是扩展类加载器的子类加载器,它负责加载应用程序的类,如自定义的类或第三方提供的类库等。它也是一个Java类,叫做sun.misc.Launcher$AppClassLoader。
除了这三种内置的类加载器外,还可以自定义类加载器,只要继承java.lang.ClassLoader抽象类,并重写其中的findClass方法即可。自定义的类加载器通常会作为应用类加载器的子类加载器。
双亲委派模型的工作原理是:当一个类需要被加载时,首先会委托给其父类加载器去尝试加载,如果父类加载器无法加载,则再由自己去尝试加载。这样就形成了一个从下到上的委托链,最终由启动类加载器作为最后的尝试者。这样做的好处是可以避免重复或冲突的类被加载,保证了Java程序的安全性和稳定性。
双亲委派模型示意图
双亲委派模型的代码在java.lang.ClassLoader类中的loadClass函数中实现,其逻辑如下:
- 首先检查类是否被加载;
- 若未加载,则调用父类加载器的loadClass方法;
- 若该方法抛出ClassNotFoundException异常,表示父类加载器无法加载,则当前类加载器调用findClass加载类;
- 若父类加载器可以加载,则直接返回Class对象
如何打破双亲委派模型?
举个栗子🌰
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 这里可以自定义类的加载方式,例如从文件系统加载类文件等
return super.findClass(name);
}
}
表演结束,谢谢大家😁,喜欢的别吝啬一个赞哈,谢了