什么是类加载器?
通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”。
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性。
类相等机制?
相等的规则:
这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括了使用instanceof关键字做对象所属关系判定等各种情况。
注意,是类相等机制。不是对象相等。如果一个类文件class文件A,由同一个虚拟机,两个不同的类加载器加载,那么这就是两个不同的类。
实例:
public class TestClassLoader {
public static void main(String[] args) {
ClassLoader classLoader1 = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
String fileName = name.substring(name.lastIndexOf("." )+1)+".class";
InputStream inputStream = getClass().getResourceAsStream(fileName);
if (inputStream == null) {
return super.loadClass(name);
}
try {
byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes);
return defineClass(name,bytes,0,bytes.length);
} catch (IOException e) {
e.printStackTrace();
}
return super.loadClass(name);
}
};
try {
Class<?> aClass = classLoader1.loadClass("com.chengjianyu.classloader.demo.TestClassLoader");
Object o = aClass.newInstance();
System.out.println(o.getClass());
System.out.println(o instanceof TestClassLoader);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
输出的结果:
class com.chengjianyu.classloader.demo.TestClassLoader
false
原因:
这是因为Java虚拟机中同时存在了两个TestClassLoader类,一个是由虚拟机的应用程序类加载器所加载的,另外一个是由我们自定义的类加载器加载的,虽然它们都来自同一个Class文件,但在Java虚拟机中仍然是两个互相独立的类,做对象所属类型检查时的结果自然为false。
三层类加载器
启动类加载器(Bootstrap Class Loader)
这个类加载器负责加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机的内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理,那直接使用null代替即可。
扩展类加载器(Extension Class Loader)
这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。
应用程序类加载器(Application Class Loader)
这个类加载器由sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystem-ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。
双亲委派模型
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。
双亲委派模型的工作过程:
1)某一个类加载器收到加载类的请求,首先不会自己加载,而是把请求委派给父类进行加载。
2)父类加载器收到加载请求后,会继续委派。每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中。
3)启动类加载器收到加载请求,尝试加载,如果完成加载则返回。如果无法完成加载,则由子类加载器尝试完成加载。
好处:
一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。
假如说,我自己搞了一个Java.lang.Object,按照双亲委派原则,我的最终会被bootstrapClassLoader加载,但是rt.jar里面已经有这个类了,肯定会率先加载rt.jar下的,所以我的永远无法被加载到。
ClassLoader加载代码片段:
破坏双亲委派模型
第一个破坏的场景:
一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器来完成加载(在JDK 1.3时加入到rt.jar的),肯定属于Java中很基础的类型了。但JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,那该怎么?(因为jndi是在rt.jar中,但是这些spi的实现在各自的包,不再rt.jar中,如何loadClass呢?)
第二个破坏的场景:热加载机制,这个就有点复杂了。
解决办法:
线程上下文类加载器(Thread Context ClassLoader)。
这个类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
例子:
dubbo内这里就是采用设置Thread的classLoader去加载所有invoker的spi的实现
Tomcat类加载实现
网上已经有一个非常详细的文章,参考:https://www.cnblogs.com/aspirant/p/8991830.html
摘抄一段过来:
- commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
- catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
- sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
- WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见
Tomcat的类加载机制是违反了双亲委托原则的,对于一些未加载的非基础类(Object,String等),各个web应用自己的类加载器(WebAppClassLoader)会优先加载,加载不到时再交给commonClassLoader走双亲委托。具体的加载逻辑位于WebAppClassLoaderBase.loadClass()
方法中,代码篇幅长,这里以文字描述加载一个类过程:
- 先在本地缓存中查找是否已经加载过该类(对于一些已经加载了的类,会被缓存在
resourceEntries
这个数据结构中),如果已经加载即返回,否则 继续下一步。 - 让系统类加载器(AppClassLoader)尝试加载该类,主要是为了防止一些基础类会被web中的类覆盖,如果加载到即返回,返回继续。
- 前两步均没加载到目标类,那么web应用的类加载器将自行加载,如果加载到则返回,否则继续下一步。
- 最后还是加载不到的话,则委托父类加载器(Common ClassLoader)去加载。
第3第4两个步骤的顺序已经违反了双亲委托机制,除了tomcat之外JDBC,JNDI,Thread.currentThread().setContextClassLoader();
等很多地方都一样是违反了双亲委托。
总结
1)什么是类加载器,同一个class文件,不同类加载器进行加载,得到的也是不同的class对象。
2)三层类加载器(BootsrtapClassLoader, ExtClassLoader, AppClassLoader),双亲委派机制机制,首先自己不加载,交给父类去加载,父类加载不到再由子类加载。
3)spi机制,需要破坏双亲委派,通过线程上下文加载器进行解决。
4)tomcat的类加载机制,从一定程度上是破坏了双亲委派机制,因为webAppClassloader,会率先自己加载而不是交由父类。