04类加载器的详解
03讲解了什么是类的声明周期,本文章将详细讲解类加载器的内容。
1.类加载器的分类
总的分为两类,一类是java代码实现的,一类是java虚拟机底层源码实现的。

类加载器的设计JDK8和8之后的版本差别较大,JDK8及之前的版本中默认的类加载器有如下几种:
- 底层源码实现的启动类加载器BootStrap
- 扩展类加载器Extension
- 应用程序类加载器Application
类加载器的详细信息可以在arthas中通过classloader命令查看:

启动类加载器(BootstrapClassLoader)是由Hotspot虚拟机提供的、使用C++编写的类加载器。默认加载Java安装目录/jre/lib下的类文件,比如rt.jar,tools.jar,resources.jar等。
通过启动类加载器去加载用户jar包:
- 放入jre/lib下进行扩展,不推荐,尽可能不要去更改JDK安装目录中的内容,会出现即时放进去由于文件名不匹配的问题也不会正常地被加载
- 使用参数进行扩展,推荐,使用-Xbootclasspath/a:jar包目录/jar包名 进行扩
扩展类加载器和应用程序类加载器都是JDK中提供的、使用Java编写的类加载器。他们的源码都位于sun.misc.Launcher中,是一个静态内部类。继承自URLClassLoader。能通过目录或指定jar包将字节码文件加载到内存中。

扩展类加载器(Extension Class Loader)是JDK中提供的、使用Java编写的类加载器。默认加载Java安装目录/jre/lib/ext下的类文件。
和上面的启动类加载器一样,如果想通过通过扩展类加载器去加载用户jar包,建议使用-Djava.ext.dirs=jar包目录 进行扩展,这种方式会覆盖掉原始目录,可以用;(windows) :(macos/linux)追加上原始目录。
arthas中类加载器的加载路径可以通过classloader -c hash查看:

2.双亲委派机制
在Java中如何使用代码的方式去主动加载一个类呢?
方式1:使用Class.forName方法,使用当前类的类加载器去加载指定的类。
方式2:获取到类加载器,通过类加载器的loadClass方法指定某个类加载器加载。
在Idea中测试下面的案例:

每个Java实现的类加载器中保存了一个成员变量叫“父”(Parent)类加载器,可以理解为它的上级,
并不是继承关系。应用程序类加载器的parent父类加载器是扩展类加载器,而扩展类加载器的parent是空。启动类加载器使用C++编写,没有上级类加载器。(这个Bootstrap是底层虚拟机的内容,不能获取到,但是parent是null就代表该类的父类是Bootstrap)

类加载器的继承关系可以在arthas通过classloader–t 查看:

双亲委派机制就是在加载一个类时,先去父亲处查找是否已经加载过了,如果加载过了,就返回已经加载过,如果没有加载过,就从上向下查找是否在当前加载器的加载目录中,如果在,就加载,如果没在,就向下委派,让子类尝试加载。

双亲委派机制解决了两个问题:
- 如果一个类重复出现在三个类加载器的加载位置,应该由谁来加载?启动类加载器加载,根据双亲委派机制,它的优先级是最高的。
- 在自己的项目中去创建一个java.lang.String类,会被加载吗?不能,会交由启动类加载器加载在rt.jar包中的String类。这样可以防止核心类被重写。
这样就能1.避免重复加载,2.保证类加载的安全性。
3.打破双亲委派机制
打破双亲委派机制主要有以下三种方法。
3.1自定义类加载器
一个Tomcat程序中是可以运行多个Web应用的,如果这两个应用中出现了相同限定名的类,比如Servlet类,Tomcat要保证这两个类都能加载并且它们应该是不同的类。如果不打破双亲委派机制,当应用类加载器加载Web应用1中的MyServlet之后,Web应用2中相同限定名的MyServlet类就无法被加载了。

Tomcat使用了自定义类加载器来实现应用之间类的隔离。每一个应用会有一个独立的类加载器加载对应的类。

先来分析ClassLoader的原理,ClassLoader中包含了4个核心方法。双亲委派机制的核心代码就位于loadClass方法中。

打破双亲委派机制的核心就是将下边这一段代码重新实现。

两个自定义类加载器加载相同限定名的类,不会冲突吗?不会冲突,在同一个Java虚拟机中,只有相同类加载器+相同的类限定名才会被认为是同一个类。
3.2线程上下文类加载器
这种方法常用于java自己实现一些底层功能,比如实现JDBC。
JDBC中使用了DriverManager来管理项目中引入的不同数据库的驱动,比如mysql驱动、oracle驱动。
下面就是使用DriverManager管理了自己下载下来的mysql的驱动jar包。

DriverManager类位于rt.jar包中,由启动类加载器加载。

依赖中的mysql驱动对应的类,由应用程序类加载器来加载。

DriverManager属于rt.jar是启动类加载器加载的。而用户jar包中的驱动需要由应用类加载器加载,这就违反了双亲委派机制。(违反点在于:一个已经被父加载器(Bootstrap ClassLoader)成功加载的类(DriverManager),在它的初始化代码(静态块)或方法执行过程中,需要去加载一个具体的JDBC驱动类。按照严格的双亲委派规则,这个加载请求应该首先委托给DriverManager自己的加载器,也就是Bootstrap ClassLoader。但Bootstrap ClassLoader根本不认识classpath下的MySQL的jar包,所以会加载失败,导致JDBC完全无法使用。)

DriverManager怎么知道jar包中要加载的驱动在哪儿?答案是DriverManager使用了SPI机制

SPI中是如何获取到应用程序类加载器的?答案是SPI中使用了线程上下文中保存的类加载器进行类的加载,这个类加载器一般是应用程序类加载器。

- 1、启动类加载器加载DriverManager。
- 2、在初始化DriverManager时,通过SPI机制加载jar包中的myql驱动。
- 3、SPI中利用了线程上下文类加载器(应用程序类加载器)去加载类并创建对象。
- 这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,打破了双亲委派机制。

其实线程上下文类加载器的方法是否算是打破了双亲委派机制是有争议的,“线程上下文类加载器的方法是否算是打破了双亲委派机制”这种说法主要出自 周志明的《深入理解Java虚拟机》。这个部分我们不细究,感兴趣的读者可以自行查找相关争议的说法,这里我们只讲解什么是线程上下文类加载器。
3.3 OSGi模块化
OSGi实现了一整套类加载机制,但是这个OSGi已经基本不再使用了,感兴趣的读者可以自行查找资料了解。
4.JDK9之后的类加载器
由于JDK9引入了module的概念,类加载器在设计上发生了很多变化。
1.启动类加载器使用Java编写,位于jdk.internal.loader.ClassLoaders类中。Java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件。启动类加载器依然无法通过java代码获取到,返回的仍然是null,保持了统一。
2、扩展类加载器被替换成了平台类加载器(Platform Class Loader)。平台类加载器遵循模块化方式加载字节码文件,所以继承关系从URLClassLoader变成了BuiltinClassLoader,BuiltinClassLoader实现了从模块中加载字节码文件。平台类加载器的存在更多的是为了与老版本的设计方案兼容,自身没有特殊的逻辑。