类加载机制:双亲委派
当类加载器试图加载某个类的时候,除非父加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器去做。
类加载机制的特征
- 双亲委派模型
- 可见性,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的,不然,因为缺少必要的隔离,就无法利用类加载器实现容器的逻辑。
- 单一性,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,就不会在子加载器中重复加载。但是注意,类加载器“邻居”间,同一类型仍然可以被加载多次,因为互相并不可见。
类加载器
内建的类加载器
启动类加载器(Bootstrap Class-Loader)
加载 jre/lib 下面的 jar 文件,如 rt.jar。它是个超级公民,即使是在开启了 Security Manager 的时候,JDK 仍赋予了它加载的程序 AllPermission。扩展类加载器(Extension Class-Loader)
负责加载我们放到 jre/lib/ext/ 目录下面的 jar 包,这就是所谓的 extension 机制。该目录也可以通过设置 “java.ext.dirs”来覆盖。应用类加载器(Application Class-Loader)
就是加载我们最熟悉的 classpath 的内容自定义类加载器
java提供的类加载器只能够到指定的目录完成相关类的加载,若我们想对自定义目录下的类完成加载,此时就需要自定义类加载器。
类加载的过程中最后,执行到最后会调用ClassLoader的loadClass方法,那么我们可以通过自定义加载器继承ClassLoader类,使自定义的类加载器也具有loadClass方法,则调用自定义加载器的loadClass方法后也会遵循双亲委派机制,完成类的一系列传递加载过程(向上再向下)。又因为loadClass方法中的findClass方法并没有具体实现,我们继承的该方法当然也需要重写。总之,若我们不想打破既定的双亲委派机制,则只需要重写findClass方法即可。
https://www.yuque.com/azrqds/za5o1o/hn95y6#ACeR8
类加载过程
三个阶段:Load、Link、Init,即加载、链接、初始化。
加载
Load阶段读取类文件产生二进制流,并转换为特定的数据结构,初步校验 cafe babe 魔法数、常量池、文件长度、是否有父类等,然后创建对应类的java.lang.Class
实例。
将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象),这里的数据源可能是各种各样的形态,如 jar 文件、class 文件,甚至是网络数据源等;如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。
加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程
链接
Link阶段包括验证、准备、解析三个步骤。验证是更详细的校验,比如final是否合规、类型是否正确、静态变量是否合理等。准备阶段是为静态变量分配内存,并设定默认值。解析类和方法确保类与类之间的相互引用正确性,完成内存布局。
验证
JVM 需要核验字节信息是符合 Java 虚拟机规范的,否则就被认为是 VerifyError,这样就防止了恶意信息或者不合规的信息危害 JVM 的运行。验证阶段有可能触发更多 class 的加载。
准备
创建类或接口中的静态变量,并初始化静态变量的初始值。这里的“初始化”和下面的显式初始化阶段是有区别的,侧重点在于分配所需要的内存空间,不会去执行更进一步的 JVM 指令。
解析
将常量池中的符号引用(symbolic reference)替换为直接引用
初始化
Init阶段执行类构造器方法clinit
。
真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。
类加载源码
打破双亲委派机制
如何在父加载器加载的类中,去调用子加载器去加载类?
- jdk提供了两种方式,Thread.currentThread().getContextClassLoader()和ClassLoader.getSystemClassLoader()一般都指向AppClassLoader,他们能加载classpath中的类
- SPI则用Thread.currentThread().getContextClassLoader()来加载实现类,实现在核心包里的基础类调用用户代码
Tomcat类加载机制
双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当先由自己的父类加载器加载,父加载器加载不了,再自己加载。
tomcat 为了实现隔离性,没有遵守这个约定,每个WebAppClassLoader打破了双亲委派机制,它首先尝试去加载某个类,如果找不到再代理给父类加载器。
线程上下文加载器
一种类加载器的传递机制
Thread.currentThread().getContextClassLoader();
获取当前上下文的类加载器
- 解决委派双亲加载模式的缺点
- 实现了JNDI等
- 解决开发中,文件加载不到的异常
所谓线程上下文加载器,是因为这个类加载器是保存在线程私有数据里,只要是同一个线程,一旦设置了线程上下文加载器,在线程后续执行过程中就能把这个类加载器取出来用。因此Tomcat为每个web应用创建一个WebAppClassLoader类加载器,并在启动web应用的线程里设置线程上下文加载器,这样spring在启动时就将线程上下文加载器取出来,用来加载Bean。