title: Java类加载器
tag: 笔记 JVM
![image-20231205164747622](https://img-blog.csdnimg.cn/img_convert/1bb2b6ebdfe01d3c2cb71a31e70d2e99.png)
概述
JVM设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”
这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动 作的代码被称为**“类加载器”**(Class Loader
)。
类与类加载器
对于 任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每 一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相 等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
下面的程序我们将演示不同类加载器对instanceof关键字的影响:
- 首先我们自定义一个简易的类加载器:
public class MyClassLoader extends 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) {
throw new ClassNotFoundException(name);
}
}
}
该类加载器可以加载处于同一包下的类。
- 测试自定义加载器和默认加载器加载的相同类是否相同:
/**
* @author 白日
* @create 2023/12/4 21:44
* @description 实现一个简易类加载器检验不同类加载器对instanceof关键字结果的影响
*/
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
ClassLoader myClassLoader = new MyClassLoader();
Object object = myClassLoader
.loadClass("com.duan.classLoader.MyClassLoader").newInstance();
System.out.println(object.getClass());
System.out.println(object instanceof com.duan.classLoader.MyClassLoader);
}
}
输出:
class com.duan.classLoader.MyClassLoader
false
object
实例虽然确实是由MyClassLoader
类实例出的,但是此时JVM中存在了两个MyClassLoader类:
- 第一个是由自定义类加载器加载的MyClassLoader类
- 第二个是由系统默认的类加载器所加载的
因此表达式object instanceof com.duan.classLoader.MyClassLoader
返回false。
结论:只要类加载器不同,那么这两个类就必定不相同。
这个结论对后面JVM类加载器架构(双亲委派模型)的设计有很大的影响。
双亲委派模型
对于JVM来说,只存在两种不同的类加载器:
- 启动类加载器:这个这个类加载器使用C++语言实现[1],是虚拟机自身的一部分。
- 其它类加载器:独立于虚拟机外部,由Java实现并且继承自抽象类java.lang.ClassLoader。
而对于我们使用者来说,应当分为以下三种:
系统提供了 3 种类加载器:
- 启动类加载器(
Bootstrap ClassLoader
): 负责将存放在<JAVA_HOME>\lib
目录中的,并且能被虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。 - 扩展类加载器(
Extension ClassLoader
): 负责加载<JAVA_HOME>\lib\ext
目录中的所有类库,开发者可以直接使用扩展类加载器。 - 应用程序类加载器(
Application ClassLoader
): 由于这个类加载器是 ClassLoader 中的getSystemClassLoader()
方法的返回值,所以一般也称它为“系统类加载器”。它负责加载用户类路径(classpath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
当然,如果有必要,我们可以加入自定义的类加载器来进行扩展(继承ClassLoader
类并重写loadClass
方法)。
它们的关系通常如下图所示:
![image-20231205164747622](https://img-blog.csdnimg.cn/img_convert/1bb2b6ebdfe01d3c2cb71a31e70d2e99.png)
上图各种类加载器的层次结果被称为双亲委派模型。这种模型除了最顶层的启动类加载器外,其它所有的类加载器都有自己的父类。
注意:这里的父类并不是由继承实现,而是组合来实现,也就是子类持有父类的引用。
这种模型并不是强制约束的模型,而是Java设计者推荐给开发者的实现类加载器的最佳实践,如果有必要的话,是可以破坏双亲委派模型的。
工作流程
当一个类加载器收到类加载的请求,这个类加载器并不会自己加载这个类,而是委托给父类来加载,直到没有父类也就是说所有的类加载请求都会传送到最顶层的启动类加载器来加载,只有当父类无法加载请求时,子类才会去自己完成加载。
目的
使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。
我们之前说过,不同类加载器加载的类是不相同的。使用双亲委托模型可以使Java类库中的类都由启动类加载器来进行加载,这样可以保证Java类库中的类在各种类加载坏境下都可以保证是相同的类(都由启动类加载器加载)。而如果不使用这样的模型让各个类加载器各自去加载的话,系统中Java类库的类就会出现多个,也就不能保持Java体系中最基础的行为。
双亲委派模型实现
双亲委派模型对Java程序的稳定运行非常重要,而它的实现却并不复杂:
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//1 首先检查类是否被加载
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//2 没有则调用父类加载器的loadClass()方法;
c = parent.loadClass(name, false);
} else {
//3 若父类加载器为空,则默认使用启动类加载器作为父加载器;
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
//4 若父类加载失败,则由当前类加载器进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
这段代码的逻辑非常清晰:
- 首先检查类是否被加载
- 没有则调用父类加载器的loadClass()方法;
- 若父类加载器为空,则默认使用启动类加载器作为父加载器;
esolve) {
resolveClass©;
}
return c;
}
这段代码的逻辑非常清晰:
1. 首先检查类是否被加载
2. 没有则调用父类加载器的loadClass()方法;
3. 若父类加载器为空,则默认使用启动类加载器作为父加载器;
4. 若父类加载失败,则由当前类加载器进行类加载