关于Java的类加载机制
什么是类加载
类加载分为三个步骤:加载、连接、初始化
加载
类加载指的是将class文件读入内存,并为之创建一个java.lang.Class对象,即程序中使用任何类时,系统都会为之建立一个java.lang.Class对象,系统中所有的类都是java.lang.Class的实例。
类的加载由类加载器完成,JVM提供的类加载器叫做系统类加载器,此外还可以通过继承ClassLoader基类来自定义类加载器。
通常可以用如下几种方式加载类的二进制数据:
1、从本地文件系统加载class文件。
2、从JAR包中加载class文件,如JAR包的数据库驱动类。
3、通过网络加载class文件。
4、把一个Java源文件动态编译并执行加载。
连接
连接阶段负责把类的二进制数据合并到JRE中,其又可分为如下三个阶段:
- 验证:确保加载的类信息符合JVM规范,无安全方面的问题。
- 准备:为类的静态Field分配内存,并设置初始值。
- 解析:将类的二进制数据中的符号引用替换成直接引用。
初始化
该阶段主要是对静态Field进行初始化,在Java类中对静态Field指定初始值有两种方式:
- 声明时即指定初始值,如static int a = 5;
- 使用静态代码块为静态Field指定初始值,如:static{ b = 5; }
JVM初始化一个类包含以下几个步骤:
- 假如这个类还没有被加载和连接,则程序先加载并连接该类。
- 假如该类的直接父类还没有被初始化,则先初始化其直接父类。
- 假如类中有初始化语句,则系统依次执行这些初始化语句。
所以JVM总是先初始化java.lang.Object类。
类初始化的时机(对类进行主动引用时):
1、创建类的实例时(new、反射、反序列化)。
2、调用某个类的静态方法时。
3、使用某个类或接口的静态Field或对该Field赋值时。
4、使用反射来强制创建某个类或接口对应的java.lang.Class对象,如Class.forName(“Person”)
5、初始化某个类的子类时,此时该子类的所有父类都会被初始化。
6、直接使用java.exe运行某个主类时。
类加载器及其机制
类加载器
当JVM启动时,会形成有3个类加载器组成的初始类加载器层次结构:
1、Bootstrap ClassLoader:根类(或叫启动、引导类加载器)加载器
它负责加载Java的核心类(如String、System等)。它比较特殊,因为它是由原生C++代码实现的,并不是java.lang.ClassLoader的子类
2、Extension ClassLoader:扩展类加载器。
它负责加载JRE的扩展目录(%JAVA_HOME%/jre/lib/ext)中JAR包的类,我们可以通过把自己开发的类打包成JAR文件放入扩展目录来为Java扩展核心类以外的新功能。
3、System ClassLoader(或Application ClassLoader):系统类加载器。
它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader来获取系统类加载器
类加载机制
JVM类加载机制主要有以下三种:
全盘负责:当一个类加载器加载某个Class时,该Class所依赖和引用的其它Class也将由该类加载器负责载入,除非显式的使用另外一个类加载器来载入。
双亲委派:当一个类加载器收到了类加载请求,它会把这个请求委派给父(parent)类加载器去完成,依次递归,因此所有的加载请求最终都被传送到顶层的启动类加载器中。只有在父类加载器无法加载该类时子类才尝试从自己类的路径中加载该类。(注意:类加载器中的父子关系并不是类继承上的父子关系,而是类加载器实例之间的关系。)
缓存机制:缓存机制会保证所有加载过的Class都会被缓存,当程序中需要使用某个类时,类加载器先从缓冲区中搜寻该类,若搜寻不到将读取该类的二进制数据,并转换成Class对象存入缓冲区中。这就是为什么修改了Class后需重启JVM才能生效的原因。
双亲委派原则
我们一般认为上一层加载器是下一层加载器的父加载器,所以,
除了BootstrapClassLoader之外,所有的加载器都是有父加载器的。
那么,所谓的双亲委派机制,指的就是:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
什么情况下父加载器会无法加载某一个类呢?
我们先来看看这四种类型的加载器的各自的职责:
- Bootstrap ClassLoader ,主要负责加载Java核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
- Extention ClassLoader,主要负责加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
- Application ClassLoader ,主要负责加载当前应用的classpath下的所有类
- User ClassLoader , 用户自定义的类加载器,可加载指定路径的class文件
那么也就是说,一个用户自定义的类,是无论如何也不会被Bootstrap和Extention加载器加载的。
为什么需要双亲委派
因为类加载器之间有严格的层次关系,那么也就java类也随之具备了层次关系。
比如一个定义在java.lang包下的类,因为它被存放在rt.jar之中,所以在被加载过程汇总,会被一直委托到Bootstrap ClassLoader,最终由Bootstrap ClassLoader所加载。
而一个用户自定义的类,他也会被一直委托到Bootstrap ClassLoader,但是因为Bootstrap ClassLoader不负责加载该类,那么会在由Extention ClassLoader尝试加载,而Extention ClassLoader也不负责这个类的加载,最终才会被Application ClassLoader加载。
这种机制有几个好处:
1、通过委派的方式,可以避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。
2、通过双亲委派的方式,还保证了安全性。因为Bootstrap ClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.Integer,那么这个类是不会被随意替换的,除非有人跑到你的机器上, 破坏你的JDK。就可以避免有人自定义一个有破坏功能的java.lang.Integer被加载。这样可以有效的防止核心Java API被篡改。
这里需要明确一下,双亲委派模型中,类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码的。
如何实现双亲委派
双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现并不复杂。
实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中:
1 | protected Class<?> loadClass(String name, boolean resolve) |
代码主要就是以下几个步骤:
1、先检查类是否已经被加载过
2、若没有加载则调用父加载器的loadClass()方法进行加载
3、若父加载器为空则默认使用启动类加载器作为父加载器。
4、如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
打破双亲委派机制
何为打破?
举个例子 有个类 Artisan
我们希望通过自定义加载器 直接从某个路径下读取Artisan.class . 而不是说 通过自定义加载器 委托给 AppClassLoader ——> ExtClassLoader —-> BootClassLoader 这么走一遍,都没有的话,才让自定义加载器去加载 Artisan.class . 这么一来 还是 双亲委派。
我们期望的是 Artisan.class 及时在 AppClassLoader 中存在,也不要从AppClassLoader 去加载。
说白了,就是 直接让自定义加载器去直接加载Artisan.class 而不让它取委托父加载器去加载,不要去走双亲委派那一套。
如何打破?
重写ClassLoader#loadClass方法
1 | public class MyClassLoaderTest { |
但是运行会报错说Object.class 找不到 。为啥呢? 你加载Boss1的时候, Boss1的父类也需要被加载, 你又把双亲委派给关了, 这个自定义的加载器在本地路径下是找不到Object.class的 。
所以换个思路 ,自己的类路径下的对象走我自己的classLoader, 其他的类 还是走双亲委派
1 | if ("com.alias.facadePattern.Boss1".equals(name)){ |
这个时候我们在AppClassLoader加载的路径下 再创建个Boss1 (如果走的还是双亲委派,那加载器肯定还是AppClassLoader)
看是不是这个Boss1 还是被自定义的ClassLoader加载,如果是,说明打破成功。
最后运行成功
如何自定义类加载器
ClassLoader中和类加载有关的方法有很多,前面提到了loadClass,除此之外,还有findClass和defineClass等,那么这几个方法有什么区别呢?
loadClass()
- 就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中。
findClass()
- 根据名称或位置加载.class字节码
definclass()
- 把字节码转化为Class
- 我们前面说过,当我们想要自定义一个类加载器的时候,并且像破坏双亲委派原则时,我们会重写loadClass方法。
那么,如果我们想定义一个类加载器,但是不想破坏双亲委派模型的时候呢?
这时候,就可以继承ClassLoader,并且重写findClass方法。findClass()方法是JDK1.2之后的ClassLoader新添加的一个方法。
1 | protected Class<?> findClass(String name) throws ClassNotFoundException { |
可见这个方法只抛出了一个异常,没有默认实现。
所以,如果你想定义一个自己的类加载器,并且要遵守双亲委派模型,那么可以继承ClassLoader,并且在findClass中实现你自己的加载逻辑即可。
一些双亲委派被破坏的例子
1、JNDI、JDBC等需要加载SPI接口实现类的情况。
2、为了实现热插拔热部署工具。为了让代码动态生效而无需重启,实现方式时把模块连同类加载器一起换掉就实现了代码的热替换。
3、tomcat等web容器的出现。
4、OSGI、Jigsaw等模块化技术的应用。
为什么JNDI,JDBC等需要破坏双亲委派
我们日常开发中,大多数时候会通过API的方式调用Java提供的那些基础类,这些基础类时被Bootstrap加载的。
但是,调用方式除了API之外,还有一种SPI的方式。
如典型的JDBC服务,我们通常通过以下方式创建数据库连接:
1 | Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "123456"); |
在以上代码执行之前,DriverManager会先被类加载器加载,因为java.sql.DriverManager类是位于rt.jar下面的 ,所以他会被根加载器加载。
类加载时,会执行该类的静态方法。其中有一段关键的代码是:
1 | ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); |
这段代码,会尝试加载classpath下面的所有实现了Driver接口的实现类。
那么,问题就来了:
DriverManager是被根加载器加载的,那么在加载时遇到以上代码,会尝试加载所有Driver的实现类,但是这些实现类基本都是第三方提供的,根据双亲委派原则,第三方的类不能被根加载器加载。
那么,怎么解决这个问题呢?
于是,就在JDBC中通过引入ThreadContextClassLoader(线程上下文加载器,默认情况下是AppClassLoader)的方式破坏了双亲委派原则。
我们深入到ServiceLoader.load方法就可以看到:
1 | public static <S> ServiceLoader<S> load(Class<S> service) { |
为什么tomcat要破坏双亲委派
我们知道,Tomcat是web容器,那么一个web容器可能需要部署多个应用程序。
不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。
如多个应用都要依赖alias.jar,但是A应用需要依赖1.0.0版本,但是B应用需要依赖1.0.1版本。这两个版本中都有一个类是com.alias.Test.class。
如果采用默认的双亲委派类加载机制,那么是无法加载多个相同的类。
所以,Tomcat破坏双亲委派原则,提供隔离的机制,为每个web容器单独提供一个WebAppClassLoader加载器。
Tomcat的类加载机制:为了实现隔离性,优先加载 Web 应用自己定义的类,所以没有遵照双亲委派的约定,每一个应用自己的类加载器——WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载,这和双亲委派刚好相反。