类加载器
从Java虚拟机的角度上讲,其实只存在两种不同的类加载器,一是启动类加载器(Bootstrap ClassLoader
),其为虚拟机的一部分,二是其他所有的类加载器。
但是从开发人员的角度上说,类加载器可以分得更为细致。
一般认为上一层加载器是下一层加载器的父加载器,因而,除了BootstrapClassLoader
之外,所有的加载器都是有父加载器的。
启动类加载器(引导类加载器,Bootstrap ClassLoader)
这个类加载使用C/C++语言实现的,嵌套在JVM内部。
它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
并不继承自ava.lang.ClassLoader,没有父加载器。
加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
扩展类加载器(Extension ClassLoader)
Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
派生于ClassLoader类
父类加载器为启动类加载器
从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/1ib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
应用程序类加载器(系统类加载器,AppClassLoader)
java语言编写,由sun.misc.LaunchersAppClassLoader实现
派生于ClassLoader类
父类加载器为扩展类加载器
它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
通过ClassLoader#getSystemclassLoader() 方法可以获取到该类加载器
用户自定义类加载器
主要用途:隔离加载类、修改类加载的方式、扩展加载源、防止源码泄漏
补充:获取ClassLoader的途径
获取当前ClassLoader
clazz.getClassLoader()
获取当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
获取系统的ClassLoader
ClassLoader.getSystemClassLoader()
获取调用者的ClassLoader
DriverManager.getCallerClassLoader()
双亲委派机制
定义:
当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
好处:
1、因为类加载器之间有严格的层次关系,那么Java的类也随之具备了一种带优先级的层次关系。
2、通过双亲委派的方式,还保证了安全性(因为Bootstrap ClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.Object,那么这个类是不会被随意替换的,可以避免有人自定义一个有破坏功能的java.lang.Object被加载,也就是沙箱安全机制)
“父子加载器”之间的关系是继承吗?
不是!!
类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码的
双亲委派是怎么实现的?
在java.lang.ClassLoader的loadClass()方法的短短数十行之中有很清晰地描述
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
具体过程从代码不难看出:
1、先检查类是否已经被加载过
2、若没有加载则调用父加载器的loadClass()
方法进行加载
3、若父加载器为空则默认使用启动类加载器作为父加载器。
4、如果父类加载失败,抛出ClassNotFoundException异常后,再调用自定义的findClass()
方法进行加载。
如果我们自定义的类加载器不希望破坏双亲委派机制,那么只需要重写
ClassLoader
的findClass
方法即可,在这个方法中,我们可以自定义类的查找顺序,根据某种规则查找类。
破坏双亲委派机制
从上面可以看到,因为他的双亲委派过程都是在loadClass()
方法中实现的,那么想要破坏这种机制,那么就自定义一个类加载器,重写其中的loadClass()
方法,使其不进行双亲委派即可
双亲委派被破坏的例子:
1、双亲委派出现之前。
由于双亲委派模型是在JDK1.2之后才被引入的,而在这之前已经有用户自定义类加载器在用了。所以,这些是没有遵守双亲委派原则的。
2、JNDI、JDBC等需要加载SPI接口实现类的情况。
这种情况是基础类型想要调用回用户的代码(而根据双亲委派机制,越基础的类由越上层的加载器进行加载) => 引入线程上下文类加载器(Thread Context ClassLoader)
3、为了实现热插拔热部署工具。
为了让代码动态生效而无需重启,实现方式时把模块连同类加载器一起换掉就实现了代码的热替换。
4、Tomcat等web容器的出现。
5、OSGI、Jigsaw等模块化技术的应用。
下面以JNDI、JDBC、Tomcat展开解释为什么要破坏双亲委派机制
JNDI,JDBC破坏双亲委派
我们日常开发中,大多数时候会通过API的方式调用Java提供的那些基础类,这些基础类时被Bootstrap加载的。但是,调用方式除了API之外,还有一种SPI的方式。
例如典型的JDBC,需要以以下的方式创建数据库链接:
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "1234");
原生的JDBC中的类是放在rt.jar包的,是由启动类加载器进行类加载的(即上述的DriverManager
类是被Bootstrap ClassLoader
加载的),原生的JDBC中Driver驱动本身只是一个接口,并没有具体的实现,JDBC中的Driver类中需要动态去加载不同数据库类型的Driver类,如mysql的mysql-connector-.jar中的Driver类是用户自己写的代码,那启动类加载器肯定是不能进行加载的,既然是自己编写的代码,那就需要由应用程序启动类去进行类加载
于是,就在JDBC中通过引入ThreadContextClassLoader(线程上下文加载器,默认情况下是AppClassLoader)的方式破坏了双亲委派原则。
Tomcat破坏双亲委派
由于Tomcat是web容器,那么一个web容器可能需要部署多个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。因而,如果采用默认的双亲委派类加载机制,那么是无法加载多个相同的类。
所以,Tomcat破坏双亲委派原则,提供隔离的机制,为每个web容器单独提供一个WebAppClassLoader加载器。
所以,为了实现隔离性,每一个应用自己的类加载器——WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给父类加载器加载,这和双亲委派刚好相反
参考
- 《深入理解Java虚拟机》周志明
- 我竟然被“双亲委派”给虐了 - 知乎