定义
Java虚拟机通过一个类的全限定名来获取类的二进制字节流的这个动作,放到虚拟机外部实现,让应用程序自己决定如何获取所需的类。
实现这个动作的代码叫做类加载器。
类与类加载器
- 每个类加载器都有自己独立的类名称空间。
- 2个类相同的判定条件:相同的Class文件被同一个类加载器加载。
如下是自定义类加载器示例:
/**
* 类加载器测试
* @Author: mango
* @Date: 2022/6/19 11:09 上午
*/
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if(is == null) {
return super.loadClass(name);
}
try {
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name,b,0,b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = classLoader.loadClass("org.mango.demo._case.classloading.ClassLoaderTest");
System.out.println(obj);
System.out.println(obj instanceof org.mango.demo._case.classloading.ClassLoaderTest);
while (true){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 结果:2个类加载器加载的同一个类对象,是不相等的
* class org.mango.demo._case.classloading.ClassLoaderTest
* false
*/
双亲委派模型
在JDK9之前,类加载组织结构如下图:
类加载器之间的层次关系被称为类加载器的双亲委派模型,子加载器采用组合方式使用父加载器。
双亲委派模型的工作流程
如果一个类加载器收到了类加载请求,先判断类是否已经被加载过,如果没有被加载会把请求委派给父加载器去完成,当父加载器无法完成加载时(它的搜索范围没有找到所需的类),子加载器才会尝试去完成加载。
以下是源码部分: java.lang.ClassLoader的loadClass()方法
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 先检查类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 调用父类加载器来加载
c = parent.loadClass(name, false);
} else {
// 如果父类加载器为空,则默认调用Bootstrap类加载器来加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 没有找到,则调用自己类加载器来加载
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;
}
}
破坏双亲委派
-
远古时代
JDK1.2之后才引入了双亲委派模型,但是类加载器和和抽象类ClassLoader在第一个版本就存在,所以为了兼容1.2之前的用户自定义的类加载器,JDK1.2的ClassLoader种新增了protected的findClass方法,并引导用户编写类加载器时重新findClass而不是loadClass。因为loadClass方法里的逻辑就是双亲委派的逻辑实现。 -
JDNI服务时代
双亲委派模型很好地解决了各个类加载器在加载基础类型的一致性问题,但是如果是基础类中需要调用用户类的代码呢?(典型场景就是JNDI服务)
JNDI已经是Java标志服务,它的代码由启动类加载器加载完成(JDK1.3中在rt.jar中),属于很基础的类型。但JNDI是为了对资源的管理和查找,它需要调用其他厂商实现并部署在用户Classpath下的JNDI服务提供者(Service Provider Interface)接口的代码。
为了解决这个问题,Java设计团队引入了线程上下文类加载器(Context ClassLoader)。
这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方 法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内 都没有设置过的话,那这个类加载器默认就是应用程序类加载器(AppClassLoader)。
JNDI服务使用线程上下文类加载器取加载所需的SPI服务的代码,是在Bootstrap类加载器中调用了应用类加载器去加载用户类路径里的代码,打破了双亲委派。
其中JNDI、JDBC等涉及SPI的服务都是采用线程上下文方式处理的,到JDK6之后引入了ServiceLoader
加上META-INFO/services
中配置信息的方案完善SPI服务的加载。
- 追求程序动态性时代
如代码热替换(Hot Swap)、模块热部署(Module Deployment)等。
IBM的OSGI实现了模块化热部署,对类加载机制做了改进。(Java9才推出了模块化,才算是稳住)
参考资料
- 周志明 * 《深入理解Java虚拟机》