讲个故事:
以前,爱捣鼓的小明突然灵机一动,写出了下面的代码
package java.lang;
public class String {
//...复制真正String的其他方法
public boolean equals(Object anObject) {
sendEmail(xxx);
return equalsReal(anObject);
}
//...
}
这样,只要引用 java.lang.String 的人,小明能随时收到他的系统的相关信息,这简直是个天才的注意。然而实施的时候却发现,JVM并没有加载这个类。
这是为什么呢?
小明能想到的事情,JVM设计者也肯定能想到。
上述故事纯属瞎编,不过,这确实是以前JVM存在的一个问题,这几天看Tomcat源代码的时候,发现频繁出现 ClassLoader 为什么要用这个东西呢?
想要解答这个问题,得先了解一个定义:双亲委派模型。
这个词第一次看见是在《深入理解JVM》中,目的也是为了解决上面所提出来的问题。
在JVM中,存在三种类型的类加载器:
Navicat )代码类的加载器,它负责装入 %JAVA_HOME%/lib 下面的类。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。 ExtClassLoader 实现,负责加载 %JAVA_HOME/lib/ext% 或者系统变量 java.ext.dir (可使用 System.out.println("java.ext.dir")查看 )指定的类加载到内存中 AppClassLoader 实现,负责加载系统类(环境变量 %CLASSPATH% )指定,默认为当前路径的类加载到内存中。 除去以上三种外,还有一种比较特殊的线程上下文类加载器。存在于 Thread 类中,一般使用方式为 new Thread().getContextClassLoader()
可以看出来,三种类型的加载器负责不同的模块的加载。那怎么才能保证我所使用的 String 就是JDK里面的 String 呢?这就是双亲委派模型的功能了:
上面三种类加载器中,他们之间的关系为:
也就是 Bootstrap ClassLoader 作为 Extension ClassLoader 的父类,而 Extension ClassLoader 作为 Application ClassLoader 的父类, Application ClassLoader 是作为 User ClassLoader 的父类的。
而双亲委派机制规定:当某个特定的类加载在接收到类加载的请求的时候,首先需要将加载任务委托给父类加载器,依次递归到顶层后,如果最高层父类能够找到需要加载的类,则成功返回,若父类无法找到相关的类,则依次传递给子类。
补充:
ClassLoader.loadClass() 或 Class.ForName(xxx,true,classLoader) 指定某个加载器加载类 instanceof 依然会返回 false 可以看到,通过双亲委派机制,能够保证使用的类的安全性,并且可以避免类重名的情况下JVM存在多个相同的类名相同,字节码不同的类。
回到刚开始讲的故事,虽然小明自定义了 String ,包名也叫 java.lang ,但是当用户使用 String 的时候,会由普通的 Application ClassLoader 加载 java.lang.String ,此时通过双亲委派,类加载请求会上传给 Application ClassLoader 的父类,直到传递给 Bootstrap ClassLoader ,而此时, Bootstrap ClassLoader 将在%JAVA_HOME%/lib中寻找 java.lang.String 而此时正好能够找到 java.lang.String ,加载成功,返回。因此小明自己写的 java.lang.String 并没有被加载。
可以看见,如果真的想要实现小明的计划,只能将小明自己编写的 java.lang.String 这个 class 文件替换到%JAVA_HOME%/lib/rt.jar 中的 String.class
到这里,估计能明白为什么需要双亲委派模型了,而某些时候,我们可以看见许多框架都自定义了 ClassLoader ,通过自定义 ClassLoader ,我们可以做很多好玩的事情,比如:设计一个从指定路径动态加载类的类加载器:
public class DiskClassLoader extends ClassLoader {
private String libPath;
public DiskClassLoader(String path){
libPath=path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try(FileInputStream fileInputStream=new FileInputStream(new File(libPath,getFileName(name)));
BufferedInputStream bufferedInputStream=new BufferedInputStream(fileInputStream);
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream()){
for (int len=0;(len=bufferedInputStream.read())!=-1;){
byteArrayOutputStream.write(len);
}
byte[] data=byteArrayOutputStream.toByteArray();
return defineClass(name,data,0,data.length);
}catch (IOException e){
e.printStackTrace();
}
return super.findClass(name);
}
private String getFileName(String name) {
int index = name.lastIndexOf('.');
if(index == -1){
return name+".class";
}else{
return name.substring(index+1)+".class";
}
}
}
上面是一个简单的例子,可以看见想要自定义 ClassLoader ,只需要继承 ClassLoader ,然后覆盖 findClass() 方法即可,其中 findClass() 是负责获取指定类的字节码的,在获取到字节码后,需要手动调用 defineClass() 加载类。
在 ClassLoader 类中,我们能找到 loadClass 的源代码:
protected Class<?> loadClass(String name, boolean resolve) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
if (c == null) {
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
在删减掉一些模板代码后,我们可以看到 loadClass() 方法就是实现双亲委派的主要代码:首先查看类是否有缓存,如果没有,就调用父类的 loadClass 方法,让父类去加载,如果父类加载失败,则自己加载,如果自己加载失败,那就返回 null ,注意: 并没有再找自己的子类去寻找类 ,也就是在哪里发起的加载,就在哪里结束。
这里可以看到, loadClass() 方法并没有被标记为 final 的,也就是我们依然可以重载它的 loadClass() 方法,破坏原本的委派双亲模型。
有些时候,双亲委派机制也会遇到一些问题,在介绍双亲委派机制的时候,我列举了一些补充。而在一些JDK中,存在一些基础 API 他们的加载由比较上层的加载器负责,这些 API 只是一些简单的接口,而具体的实现可能会由其他用户自己实现,这个时候就存在一个问题,如果这些基础的 API 需要调用/加载用户的代码的时候,会发现由于 父类无法找到子类所能加载的类 的原因,调用失败。
最典型的例子便是 JNDI 服务, JNDI 服务是在 JDK1.3 的时候放入 rt.jar 中,而 rt.jar 有 Bootstrap ClassLoader 加载, JNDI 的功能是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的 classpath 下的 JNDI 接口提供者 (SPI, Service Provider Interface) 的代码,但启动类加载器不可能“认识”之些代码,该怎么办?
这就需要用到最开始讲的特殊的加载器: 上下文类加载器
上下文类加载器的使用方式为: Thread.currentThread().getContextClassLoader()
上下文类加载器是什么意思呢?可以看源码, Thread 初始化是通过本地方法 currentThread(); 初始化的,而 classLoader 也正是通过 currentThread 初始化, currentThread 指的是当前正在运行的线程。
而默认情况下,启动 Launcher 后, Launcher 会将当前线程的上下文加载器设置为 Application ClassLoader
public Launcher() {
//...
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
//...
}
因此, 上下文类加载器默认就是系统加载器 ,通过上下文加载器,更高级别的加载器便可以调用系统加载器加载一个类。
Tomcat 作为一个Web容器,会包含各种Web应用程序,而为了使各个应用程序不互相干扰,至少需要达到以下要求:
因为这些需求,所以在Tomcat中,类的加载不能使用简单的 ClassLoader 来加载,而是需要自定义分级的 ClassLoader 。
在Tomcat中,定义了3组目录结构 /common/* , /server/* 和 /shared/* 可以存放Java类库,另外还有Web应用程序自身的结构: /WEB-INF/* ,而这几级目录结构分别对应了不同的加载器
因此,需要支持以上结构,可以通过自定义遵循双亲委派模型的 ClassLoader 来完成。
参考链接:
一看你就懂,超详细java中的ClassLoader详解
Java类加载机制与Tomcat类加载器架构
【JVM】浅谈双亲委派和破坏双亲委派
关于Java类加载双亲委派机制的思考
如果觉得写得不错,欢迎关注微信公众号:逸游Java ,每天不定时发布一些有关Java干货的文章,感谢关注