类加载器的作用是:通过一个类的全限定名来获取描述此类的二进制字节流,应用程序可以使用虚拟机提供的类加载器,也可以通过自定义类加载器来实现获取所需要的类
命名空间
对于虚拟机中的任意一个类,它的唯一性是由加载它的类加载器和它本身一同确立的。每一个类加载器都拥有一个独立的命名空间,每个被当前类加载器加载的类都会记录在该类加载器的命名空间中,每个类只能在一个类加载器中出现一次,这也意味着每个类只能被同一个类加载器加载一次,不同类加载器加载的同一个Class在虚拟机中是不同的类,这也就意味着它们在使用equals()、isAssignableFrom()、isInstance()方法比较时会返回false
类加载器的分类
从Java虚拟机实现的角度看,只有两种类加载器,一种是启动类加载器(Bootstrap ClassLoader),这个类加载器是用C++实现的,作为虚拟机自身的一部分,其它的类加载器都是由Java语言实现的,独立于虚拟机外部,并且都继承自java.lang.ClassLoader
但从Java开发的角度上来看,常用的类加载器大致有如下四种
启动类加载器(Bootstrap ClassLoader)
该加载器负责加载<JAVA_HOME>\lib目录下、或者被-Xbootclasspath参数所指定的路径下,文件名在虚拟机中记录过的类库,启动类加载器无法通过Java代码直接引用,如果在编写类加载器时,需要把加载请求委托给启动类加载器,直接使用null代替即可
扩展类加载器(Extension ClassLoader)
该加载器负责加载<JAVA_HOME>\lib\ext目录下的、或者被java.ext.dirs系统变量所指定路径下所有类库,可以被Java代码直接引用
应用程序类加载器(Application ClassLoader)
该加载器负责加载用户类路径(ClassPath)上所指定的类库,可以被Java代码引用,在没有指定自定义类加载器的情况下一般都是使用该加载器加载类,因此也被成为系统类加载器。可以使用ClassLoader的getSystemClassLoader()方法获取引用
自定义类加载器(User ClassLoader)
该加载器通过用户重写的类加载器loadClass()方法来加载类,想要从Class文件外的地方加载类,就需要编写相应的自定义类加载器
双亲委派模型
Java应用程序的类加载都是由以上四种类加载器配合进行加载的,这些类加载器间都遵循着一种层次关系,被称为双亲委派模型,如图所示:
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,但这里类加载器的父子关系一般不会用继承的关系来实现,而是通过组合的方式复用父加载器的代码
双亲委派模型并不是一个强制性的约束模型,而只是Java设计者推荐给开发者的一种好的类加载器的实现方式
双亲委派模型的工作过程是,如果一个类加载器收到了类加载的请求,它不会首先去加载它,而是把这个请求先委托给它的父加载器去完成,每一个层次的类加载器都遵循这个逻辑,因此所有的类加载请求最终都会被先委托给顶层的启动类加载器,然后从顶层的类加载器开始判断,该类是否在自己所负责的加载范围内,如果是,则加载,不是则反馈给委托的子加载器,直到某一下层的类加载器判断该类在自己所负责的加载范围内,并最终由这个加载器加载该类
双亲委派模型使得类随着它的类加载器一起具备了一种带有优先级的层次关系,例如java.lang.Object对象,在双亲委派模型下,无论哪个类加载器想要加载它,最终都会被委托给最顶端的启动类加载器,然后由于命名空间规定了一个类加载器只能加载一个类的全限定名表示的类一次,因此能够保证java.lang.Object对象在虚拟机的声明周期中永远是同一个类,避免了类加载关系的混乱
破坏双亲委派模型
由于双亲委派模型并非是虚拟机的强制约束,因此虽然大部分类加载器都实现了双亲委派模型,但它在Java发展的历史上也被破坏过,以下就是三次双亲委派模型被破坏的情况
第一次引入双亲委派模型
双亲委派模型是在JDK1.2之后才被引入的,为了兼容在之前已经存在的用户类加载器,Java设计者在java.lang.ClassLoader中引入了一个protected的方法findClass(),在加载类时会先调用loadClass()尝试加载类,如果加载失败才会使用findClass()加载类,那只要在findClass()添加符合双亲委派模型的逻辑,那就可以实现在兼容之前已经存在的用户类加载器的前提下最大限度的支持双亲委派模型
JDK8中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) {
long t0 = System.nanoTime();
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.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
线程上下文
在双亲委派模型中,越基础的类会被越高层次的类加载器加载,这引入了一个缺陷:上层类加载器加载的类无法调用下层类加载器加载的代码,因为如果加载的基础类依赖下层加载器加载的用户类,就必须要先加载该用户类的代码,但上层类加载器无法加载下层类加载器负责的类,而在双亲委派模型下,只能下层的类加载器委托上层加载类,不能由上层的类加载器委托下层加载类。一个典型的例子就是JNDI服务,它作为Java标准服务,由启动类加载器加载,但JDNI需要调用部署在应用程序ClassPath下的SPI代码。
为了解决这个问题,Java设计团队引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader),这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,在缺省的情况下默认使用父线程的线程上下文加载器,如果继承链中的线程都没有设置,那就默认使用应用程序类加载器
引入了线程上下文类加载器可以让上层的类加载器委托下层的类加载器加载类,但这显然违背了双亲委派模型的自底向上的委托流程
为实现热部署复杂化加载机制
代码热替换(Hot Swap),热部署(Hot Deployment)的需求愈发强烈,但双亲委派模型已经无法支持,因为在双亲委派模型下,加载一个类的加载器在运行时是确定的,而命名空间又限制了加载器无法加载一个类两次,这就造成了替换类没有办法被对应的加载器加载后替换源类。为了满足Java应用在运行过程中的热替换,势必需要更加复杂的模型
如今OSGi已经成为了业界“事实上”的Java模块化标准,它实现模块化热部署的关键正是它自定义了一套类加载器机制的是实现。每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉实现代码热替换
以下是OSGi环境下类加载请求的加载逻辑:
- 将以java.* 开头的类委派给父类加载器加载
- 否则,将委派列表名单内的类委派给父类加载器加载
- 否则,将import列表中的类委派给Export这个类的Bundle的类加载器加载
- 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载
- 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载
- 否则,查找Dynamic Import列表的Bundle,委派给对应的Bundle的类加载器加载
- 否则,类查找失败