类加载器:通过一个类的全限定类名来获取描述此类的二进制字节流
应用于:类层次划分、OSGi、热部署、代码加密等领域
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机的唯一性,每一个类加载器,都拥有一个独立的类命名空间。
所以在判断两个类是否“相等”时,需要先判断它们是不是由同一个类加载器加载的。
“相等”包括:
- equals() 方法
- isAssignableFrom() 方法
- isInstance() 方法
- instanceof 关键字
类加载器:
启动类加载器(Bootstrap ClassLoader)
- C++ 语言实现
- 加载
\lib 目录中,或被 -Xbootclasspath 参数所指定的路径中的类库 - 仅识别特定名字的类库,比如 rt.jar,不符合的类库不会被虚拟机加载
- 启动类加载器无法被程序引用,如果一个类的 ClassLoader 是启动类加载器,则为 null
扩展类加载器(Extension ClassLoader)
- 实现类:sun.misc.Launcher$ExtClassLoader
- 加载
\lib\ext 目录中,或被 java.ext.dirs 系统变量所指定的路径中的类库 - 开发者可以直接使用
应用程序类加载器(Application ClassLoader)
- 实现类:sun.misc.Launcher$AppClassLoader
- 可以通过调用 ClassLoader.getSystemClassLoader() 方法获得
- 加载 ClassPath 路径中的类库
- 开发者可以直接使用
- 一般情况下,是程序中默认的类加载器
双亲委派模型:
Parents Delegation Model
除了启动类加载外,每个类加载器都应当有自己的父类加载器,一般通过组合来实现。
工作过程:
如果一个类加载器收到了类加载的请求,它会先将这个请求委派给父类加载器去完成,只有当父类加载器无法完成这个请求,子类加载器才会尝试自己去加载。同样的,父类加载器也需要先将请求委派给它的父类加载器。
优点:
- Java 类随着它的类加载器一起具备了一种带有优先级的层次关系
- 确保无论哪个类加载器去加载这个类,最终都会被委派到同一个类加载器
通过重写 ClassLoader.findClass() 方法实现自定义类加载器。
ClassLoader.loadClass() 方法实现了双亲委派模型:
- 如果父类加载器不为 null,先调用父类加载器加载
- 如果为 null,使用启动类加载器加载
- 如果以上都返回 null,即父类加载器无法加载,则调用自身的 findClass() 方法加载
自定义类加载如果强行用 defineClass() 方法去强行加载 “java.lang” 开头的类,会抛出异常 “java.lang.SecurityException:Prohibited package:java.lang”
双亲委派模型的破坏:
loadClass():
JDK 1.2 之后才加入 protected 方法 findClass(),在此之前,都是重写 loadClass() 方法,因为虚拟机在进行类加载时,会调用类加载器的私有方法 loadClassInternal(),而这个方法唯一的逻辑就是调用 loadClass()
自身模型缺陷:
基础类不能回调用户的代码。即基础类的类加载器不能加载用户的类。
比如:JNDI 服务。它的代码在基础类中,由启动类加载器去加载,它需要调用由厂商提供的 SPI 代码,而 SPI 代码一般放到 ClassPath 下,启动类加载器不能加载。
解决:线程上下文类加载器(Thread Context ClassLoader)
它不是一个类加载器,而是 Thread 类中使用 contextClassLoader 来保存一个类加载器,通过 setContextClassLoader() 方法赋值。默认会从父线程继承一个,如果为手动设置过,默认就是应用程序类类加载器 AppClassLoader。
java.lang.Thread 类中的 contextClassLoader:
/* The context ClassLoader for this thread */
private ClassLoader contextClassLoader;
public void setContextClassLoader(ClassLoader cl) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("setContextClassLoader"));
}
contextClassLoader = cl;
}
JNDI 服务使用 contextClassLoader 去加载所需要的 SPI 代码,即父类加载器请求子类加载器去完成类加载器的动作。这违背了双亲委派模型,但也是无奈之举。
java.util.ServiceLoader 类中使用 contextClassLoader 加载类:
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
Java 中所有涉及 SPI 的加载动作基本上都采用这种方式,例如:JNDI、JDBC、JCE、JAXB、JBI 等
程序动态性:
代码热替换(HotSwap)、模块热部署(Hot Deployment)
OSGi 实现模块化热部署,每一个程序模块(OSGi 中成为 Bundle)都有一个自己的类加载器,当需要更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉,以实现代码的热替换。
在 OSGi 环境下,类加载器不再是双亲委派模型中的树状结构,而是更为复杂的网状结构。
当收到类加载请求时:
- 将以 java.* 开头的类委托给父类加载器加载
- 否则,将委派列表名单内的类委派给父类加载器加载
- 否则,将 Import 列表中的类委派给 Export 这个类的 Bundle 的类加载器加载
- 否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器加载
- 否则,查找类是否在自己的 Fragment Bundle 中,如果在,则委派给 Fragment Bundle 的类加载器加载
- 否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器加载
- 否则,类查找失败
Tomcat 的类加载器:
Tomcat 使用正统的类加载器架构。
一个健全的 Web 服务器,要解决以下问题:
- 部署在同一个服务器上的两个 Web 应用所使用的 Java 类库可以相互隔离。
- 不同 Web 应用可能使用不同版本的类库
- 部署在同一个服务器上的两个 Web 应用所使用的 Java 类库可以相互共享。
- 对于所有 Web 应用都需要的类库,可以共享,否则每个应用都加载一次类库,会造成内存的占用和浪费
- 服务器自身不能受 Web 应用影响,所以服务器所使用的类库要与应用的类库独立开来
- 支持 JSP 生成类的热替换。
Tomcat 提供四组 ClassPath 路径存放 Java 类库:
- /common:存放被 Tomcat 和所有 Web 应用共享的类库
- /server:存放被 Tomcat 使用的类库
- /shared:存放被所有 Web 应用共享的类库
/WebApp/WEB-INF:存放被当前 Web 应用(WebApp)使用的类库
CommonClassLoader
- /common 目录
- CatalinaClassLoader
- /server 目录
- SharedClassLoader
- /shared 目录
- WebAppClassLoader
- /WebApp/WEB-INF 目录
- JasperLoader
- 每一个 Jsp 文件对应一个 Jsp 类加载器
- JasperLoader 的加载范围仅仅是这个 JSP 文件所编译出来的那一个 Class,它出现的目的就是为了被丢弃
- 当服务器检测到 JSP 文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 Jsp 类加载器来实现 JSP 文件的 HotSwap 功能
对于 Tomcat 6.x 之后的版本,只有指定了 tomcat/conf/catalina.properties 配置文件的 server.loader 和 share.loader 项后,才会建立 CatalinaClassLoader 和 SharedClassLoader 的实例,否则会使用 CommonClassLoader 的实例代替,而默认是没有配置这两个 loader 项的。
所以 Tomcat 6.x 之后,把 /common、/server、/shared 三个目录默认合并到 /lib 目录,这个目录相当于之前的 /common 目录。用户可以自行选择开启 server.loader 和 share.loader。
如果 10 个 Web 应用都用到 Spring 的话,可以把 Spring 放到 Common 或 Shared 目录下,让这些应用共享。但 Spring 需要管理 Web 应用中的类,就需要能够加载 /webApp/WEB-INF 目录中的类,那么被 CommonClassLoader 或 SharedClassLoader 加载 Spring 要如何加载用户的类呢?ThreadContextClassLoader