类加载器

一、类加载器

1 类加载过程

虚拟机只加载程序执行时所需要的类文件。假设程序从 MyProgram.class 开始运行,下面是虚拟机执行的步骤:

  1. 虚拟机有一个用于加载类文件的机制。
  2. 如果类拥有类型为另一个类的域,或者是拥有超类,那么这些类文件也会被加载。
  3. 接着,虚拟机执行类中的 main 方法。
  4. 如果 main 方法或者 main 调用的方法要用到更多的类,那么接下来就会加载这些类。

每个Java程序至少拥有三个类加载器

  • 启动类加载器(Bootstrap ClassLoader)

    ​ 这个类加载器负责负责加载JDK中的核心类库,这个类加载器完全由JVM控制

  • 扩展类加载器(Extendsion ClassLoader)

    ​ 这个类加载器负责加载\lib\ext目录下的类库

  • 应用程序类加载器(Application ClassLoader)

    ​ 这个类加载器负责加载用户类路径(CLASSPATH)下的类库,一般我们编写的java类都是由这个类加载器加载

2 类加载器的层次结构

类加载器有一种父/子关系。除了引导类加载器外,每个类加载器都有一个父类加载器。父类加载失败子类才加载。

如果插件被打包为 JAR 文件,那就可以直接用 URLClassLoader 类的实例去加载这些类。

URL url = new URL("path");
URLClassLoader pluginLoader = new URLClassLoader(new URL[] {url});
Class<?> cl = pluginLoader.loadClass("mypackage.MyClass");

可以通过下面将其设置成为任何类加载器

Thread t = Thread.currentThread();
t.setContextClassLoader(loader);

助手方法可以获取这个上下文类加载器:

Thread t = Thread.currentThread();
ClassLoader loader = t.getCOntextClassLoader();
Class cl = loader.loaderClass(className);

当调用由不同的类加载器加载的插件类的方法时,进行上下文类加载器的设置是一种好的思路;或者,让助手方法的调用者设置上下文类加载器。

3 将类加载器作为命名空间

同一虚拟机中,可以有两个类的类名和包名都相同。类是由它的全名和类加载器来确定的。

4 编写自己的类加载器

编写自己的类加载器,只需要继承 ClassLoader 类,然后覆盖下面这个方法

findClass(String className)

ClassLoader 超类的 loadClass 方法用于将类的加载操作委托给其父类加载器去进行,只有当该类尚未加载并且父类加载器也无法加载该类时,才调用 findClass 方法。

如果要实现该方法,必须做到以下几点:

  1. 为来自本地文件系统或者其他来源的类加载其字节码
  2. 调用 ClassLoader 超类的 defineClass 方法,想虚拟机提供字节码。

5 字节码校验

当类加载器将新在加载的 Java 平台类的字节码传递给虚拟机,这些字节码首先要接受校验器的校验。校验器负责检查那些指令无法执行的明细那有破坏性的操作。出了系统类外,所有的类都要被校验。

下面是校验器执行的一些检查:

  • 变量要在使用之前进行初始化。
  • 方法调用与对象引用类型之间要匹配。
  • 访问私有类型和方法的规则没有被违反。
  • 对本地变量的访问都落在运行时堆栈内。
  • 运行时堆栈没有用溢出。

二、双亲委派原则

1 双亲委派原则

一个类加载器受到类加载的请求,它会把这个请求转交到它的父加载器去请求,如果上级还有加载器,就继续把请求上传,直到启动类加载器。然后找到就返回给子加载器,直到第一个发出请求的类加载器。如果最后还是没有找到,就让子加载器自己去找

img

2 破坏双亲委派原则

单一责任原则(SRP)不是绝对的。 它的存在有助于代码的可维护性和可读性。 但是您可能会不时看到解决方案,破坏SRP的模式,而且还可以。 其他原则也是如此,但是这次我想谈谈SRP。

在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载,而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器就是很好的选择。

2.1 线程上下文类加载器(contextClassLoader)

通过java.lang.Thread类中的getContextClassLoader()setContextClassLoader(ClassLoader cl)方法来获取和设置线程的上下文类加载器。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类和资源,如下图所示,以jdbc.jar加载为例

img

Author: iMine
Link: https://imine141.github.io/2020/08/15/Java%E5%9F%BA%E7%A1%80/%E5%AE%89%E5%85%A8/%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.