从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader
。
启动类加载器(Bootstrap ClassLoader): 负责加载存放在
目录中的核心类库,如rt.jar
、resources.jar
等(或者被 -Xbootclasspath
参数所指定的路径中的,并且是虚拟机识别的类库)。这个加载器是 C++ 编写的,随着JVM启动。
扩展类加载器(Extension ClassLoader): 负责加载
目录中的类库,(同样也可以用 java.ext.dirs
系统变量来指定路径)。
应用程序类加载器(Application ClassLoader): 负责加载用户类路径 classpath 上所有的 jar 包和 .class 文件。
自定义类加载器: 可以支持一些个性化的扩展功能。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
这里所指的“相等”,包括代表类的Class对象的 equals()
方法、isAssignableFrom()
方法、isInstance()
方法的返回结果,也包括使用 instanceof
关键字做对象所属关系判定等情况。
为了避免类的重复加载,确保一个类的全局唯一性,以及保护程序安全,防止核心API被随意篡改,JVM会采用双亲委派模型进行加载,双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。
双亲委派模型的工作过程是: 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的夹杂请求最终都应该传送到顶层的启动类加载器中,只有父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
这里类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父加载器的代码,源码如下:
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object
,它存放在 rt.jar
之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。
相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为 java.lang.Object
的类,并放在程序的 classPath
中,那系统中将会出现多个不同的 Object 类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。
双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委派的代码都集中在 java.lang.ClassLoader
的 loadClass()
方法之中,代码逻辑主要为:先检查是否已经被加载过,若没有加载则调用父加载器的 loadClass() 方法,若父加载器为空,则默认使用启动加载器作为父加载器。如果父类加载器加载失败,抛出 ClassNotFoundException 异常后,在调用自己的 findClass() 方法进行加载。
上面我们以及看过了 loadClass()
方法的源码,双亲委派的具体逻辑就实现在这个方法之中,JDK1.2之后就不提倡去覆盖 loadClass()
方法,而是应当把自己的类加载逻辑写到 findClass()
方法中,在 loadClass()
方法的逻辑里如果父类加载失败,则会调用自己的 findClass()
方法来完成加载,这样就可以保证新写出的类加载器是符合双亲委派规则的。
但是双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型,但也有例外。
Tomcat为什么会破坏双亲委派模型呢?因为Tomcat需要解决如下问题:
针对上述问题一,需要Web应用层级的隔离,Tomcat给每个Web应用都创建一个类加载器实例(WebAppClassLoader),该加载器重写了 loadClass()
方法,优先加载当前应用目录下的类,如果当前找不到了,才会一层一层往上找。
对于问题二,另外并不是Web应用程序下的所有依赖都需要隔离的,部署在同一个Web容器中相同的类库相同的版本可以共享。否则如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。
这里Tomcat就在WebAppClassLoader上加了个父类加载器(SharedClassLoader),如果WebAppClassLoader自身没有加载到某个类,那就委托SharedClassLoader去加载。
问题三中需要隔绝Web应用程序与Tomcat本身的类,Tomcat会有类加载器(CatalinaClassLoader)来装载Tomcat本身的依赖。
如果Tomcat本身的依赖和Web应用还需要共享,那么还有类加载器(CommonClassLoader)来装载进而达到共享。
JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个 .class 文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。 (JSP热部署原理)
双亲委派模型很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为他们总是作为被用户代码调用的API,但是如果我们的基础类也要调用用户代码,那该怎么办?
如常见的数据库驱动加载,在使用JDBC写程序之前,通常会调用这行代码Class.forName("com.mysql.jdbc.Driver")
,用于加载所需要的驱动类。
上述代码如果想要运行成功的话,肯定是需要引入mysql-connector-java
依赖的,如下:
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.45version>
dependency>
在引入依赖成功后,我们可以在mysql-connector-java
包下查看到的/META-INF/services/java.sql.Driver
文件
其实就是利用了Java SPI机制,文件中内容其实就是JDK中rt.jar
类库中的一个接口,如下:
按照上述介绍的rt.jar
类库应该有启动类加载器(Bootstrap ClassLoader)进行加载,但是其实该接口在rt.jar
类库是没有任何实现类的,其实现类是在引入的mysql-connector-java
包中。
这里就存在基础类也要调用用户代码,所以启动类加载器(Bootstrap ClassLoader)是无法进行加载的,那么SPI机制下JDBC是如何加载的呢?
有了解Java SPI机制的话,应该知道是使用 ServiceLoader 来加载,这里主要在 DriverManager 类中
在 DriverManager 类进行加载初始化时,一定会执行 static 代码块
这里可以看出为了解决上述问题,Java引入了一个线程上下文加载器,这个类加载器可以通过 java.lang.Thread
类的setContextClassLoader()
方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。