双亲委派模型并不是一种强制性的约束,只是JDK官方推荐的一种方式。
每⼀个类都有⼀个对应它的类加载器。系统中的 ClassLoder 在协同⼯作的时候会默认使⽤ 双亲委派模型 。
JVM启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。且在类加载的时候,系统会⾸先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。
加载的时候,他不会先去尝试自己去加载这个类,而是会把该请求委派该⽗类加载器的 loadClass() 去处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当⽗类加载器⽆法处理时(在它的加载路径下没有找到所需加载的 Class),才由⾃⼰来处理(调用自己的findclass()方法来加载类)。当⽗类加载器为null时,会使⽤启动类加载器BootstrapClassLoader 作为⽗类加载器。
7.1. 优点
1.保证JDK的核心类会被优先加载
2.保证java程序的稳定运行,可以避免类的重复加载,也保证了Java的核心API不会被篡改
3.不论是哪个类加载器去加载某个类,最终都是委托给顶层的启动类加载器去进行加载,这样就保证了使用不同的类加载器去加载最终获得的都是同一个Object对象
7.2. 破坏双亲委派机制:
自定义加载器的话,需要继承classLoader。如果我们不想打破双亲委派模型,就重写ClassLoader 类中的findclass()方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写loadclass()方法。
7.2.1. 为什么是重写 loadclass()方法打破双亲委派模型呢?
双亲委派模型的执行流程已经解释了:
类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadclass()方法来加载类)。
7.2.2. 方法:
- 可以⾃⼰定义⼀个类加载器,重写loadClass方法;
- Tomcat 可以加载自己目录下的 class 文件,并不会传递给父类的加载器;(阿里文章)
- Java 的 SPI,发起者 BootstrapClassLoader 已经是最上层了,它直接获取了 AppClassLoader 进行驱动加载,和双亲委派是相反的。
7.3. JVM 判定两个Java 类是否相同的具体规则:
JVM不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即使两个类来源于同一个class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相同。
7.4. 流程
- loadClass(): 这是类加载机制的入口点,当需要加载一个类时,这个方法会被调用。它遵循双亲委派模型,首先检查该类是否已经被当前类加载器加载过(通过查询其内部缓存)。如果没有加载过,loadClass()不会直接去查找类,而是先委托给其父类加载器去尝试加载。
- 双亲委派模型: 在loadClass()方法内部,如果父类加载器存在(非Bootstrap ClassLoader,因为它没有父加载器),则递归地调用父加载器的loadClass()方法。这一过程会一直向上委托,直到到达 Bootstrap ClassLoader。如果父加载器无法加载这个类(返回null),那么控制权会逐级返回到原始请求加载的类加载器。
- findClass(): 当委派链中的所有类加载器都无法加载这个类时,最终控制流会回到最初请求加载的类加载器,并调用其findClass()方法。这是自定义加载逻辑实现的地方,可以通过覆盖这个方法来从特定来源(如文件系统、数据库、网络等)加载类的二进制数据,并使用defineClass()方法将这些二进制数据转换为JVM可识别的Class对象。
- defineClass(): findClass()内部通常会使用defineClass()方法来完成类的定义工作,即将字节码转化为Class对象并加载到Java虚拟机中。此方法可以看作是将二进制数据“定义”为一个Java类型的点,它也是类加载过程中真正创建类的实例的地方。