在写这篇文章之前特意读了下十多年前的一本书的某些章节 《深入java虚拟机》,收获还是挺大,至少知道了类加载器在安全方面起到了至关重要的作用,废话不多说,来看看类加载器是什么。
我们知道我们写的java程序最终都要编译成class文件,这是一种二进制的文件,被设计的非常紧凑,因为这有利于class文件在网络中的传输,奠定java语言在分布式领域的优势,另一个优势是跨平台,也就是所谓的一次编译到处运行。当jvm执行class文件的时候,首先要做的肯定是去加载它,类加载器主要做的事情就是去加载class文件。JVM中的类加载机制是双亲委派,不知道为啥叫双亲,好奇怪的名字,姑且也这样叫吧,那么什么叫双亲委派呢?再解释这个概念的时候,先来看下JVM中默认提供的三种类加载器。
启动类加载器,bootstrap这个加载器由C++提供,java程序无法获取,此加载器也是加载器的祖宗,它没有父亲。确切的讲它加载的是jre/lib下面的jdk核心类库,也可以由-Xbootclasspath启动参数指定,值得一提的是该加载器只加载特定名字的jar,比如rt.jar,非法的jar它不去加载。
扩展类加载器(Extension ClassLoader),扩展类加载器由java语言本身实现,负责加载JAVA_HOME/lib/ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,我们是可以直接使用该加载器的,它是bootstrap加载器的孩子。
应用程序类加载器(Application
ClassLoader),也叫系统类加载器,一开始学习java的时候总会提到classpath的概念,没错,这个加载器就是加载我们指定的classpath下的类。它是扩展类加载器的孩子。同样是由java程序编写,我们也可以扩展它。
介绍完默认的三种类加载器,双亲委派的模型就很容易理解了,在jdk的最初版本的时候,类加载还不是这个模型,在jdk1.2的时候才正式有了这个概念。也就是当我们的系统类加载器试图通过loadClass去加载一个类的时候,会先把加载的动作传递给父加载器(如果有),就这样一层层的传递,如果最终的根加载器没有加载到该类,则依次返回,由子类加载,如果所有的类加载器都加载不到,则报ClassNotFoundException错误。之所以采用这样一个模型是因为考虑到安全性,假设我们自己定义了一个java.lang.Integer类的实现,并指定类加载器去加载,试想如果jvm加载的Integer类不是jdk提供的,而是我们自己写的,这将是非常危险的。但是采用委派的方式,我们自己定义的Integer类将永远不会得到加载的机会。
看下双亲委派源码:
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // First, check if the class has already been loaded //首先检查这个类有没有被加载,这里的name是类的权限定名 Class c = findLoadedClass(name); if (c == null) { //如果还没被加载,并且有父加载器,则委派给父加载器加载 try { if (parent != null) { c = parent.loadClass(name, false); } else { //如果没有父亲,则使用bootstrap加载,可以想象,该方法最终是个native方法 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; }
实际上我们自己扩展的类加载器可以抛弃这种双亲委派的加载模型,而且有些时候还必须要这么做,双亲委派的一个特点是由父加载器加载的类对子加载器都可见,然而反过来确不行,而父加载器一般加载的都是底层基础的api,所以大多数情况都没有问题,但是有些特殊的情况,需要底层的api去调用用户实现的类,比如JNDI或者JDBC,JNDI的代码由启动类加载器去加载,目的就是对资源进行集中的管理和查找,各个厂商的都有自己的实现位于classpath下,JNDI要去调用其中的api,关于这种情况有个统称叫Service
Provider Interface简称SPI,那么问题来了,由于父类加载器不能调用到子加载器加载的代码,而我们现在确实有这么一种诉求。为了解决这个问题,在java中有一种线程上下文加载器的概念,通过该方法可以在线程上下文设置一个加载器,Thread.currentThread().setContextClassLoader(),如果未设置会从父线程来继承classloader,如果都为设置默认为app类加载器,有了这个方法,JNDI就可以拿到这个classLoader去加载SPI代码,也就是将加载的动作由子类向上请求逆向,由父加载器委托给子类来加载,从而实现SPI代码的可见性。