Java类加载器-学习笔记

JVM内置类加载器

  • 根类加载器(BootstrapClassLoader)
    该加载器是 JVM 内建的,由 C++ 实现的,加载 JRE/lib/rt.jar 或者 -Xbootclasspath 选项指定位置的 class 文件和 jar 包。
  • 扩展类加载器(ExtClassLoader)
    加载 JRE/lib/ext/*.jar 或 -Djava.ext.dirs 指定目录下的jar包
    注意点:ExtClassLoader 默认只加载 jar 文件中的类,所以修改 JVM 参数 -Djava.ext.dirs 指定的 class 文件不会被加载。
  • 系统(应用)类加载器(AppClassLoader)
    加载 classpath 或 -Djava.class.path 所指定的目录下的类和 jar 包。
  • 用户自定义的类加载器
    要创建自定义类加载器,只需要继承 java.lang.ClassLoader 类,然后覆盖它的 findClass(String name) 方法即可,该方法根据参数指定的类的名字,返回对应的 Class 对象的引用。

JVM 规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了 .class 文件的缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)。如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

对于一个类的依赖类是由加载该类的类加载器加载。

获得ClassLoader的途径:

  • clazz.getClassLoader()
  • Thread.currentThread().getContextClassLoader()
  • ClassLoader.getSystemClassLoader()
  • DriverManager.getCallerClassLoader()

类的命名空间

每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成,具有以下特性:

  • 同一个命名空间内的类是相互可见的
  • 子加载器的命名空间包含所有父加载器的命名空间
  • 子加载器所加载的类能够访问到父加载器所加载的类
  • 父加载器所加载的类无法访问到子加载器所加载的类
  • 如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见
  • 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类

在运行期,一个 Java 类是由该类的完全限定名和用户加载该类的定义类加载器所共同决定的。如果完全限定名相同的类是由两个不同的加载器加载的,那么这些类就是不同的,即便 .class 文件的字节码完全一样,并且从相同的位置加载也是如此。

类加载的双亲(父亲)委托机制

除了Java虚拟机自带的根加载器以外,其余的类加载器都有且只有一个父加载器。当 Java 程序请求加载器 loader1 加载 Sample 类时,loader1 首先委托自己的父加载器去加载 Sample 类,若父加载器能加载,则由父加载器完成加载任务,否则才由 loader1 本身加载 Sample 类。

加载器之间的父子关系不是通过继承实现的父子关系,而是通过组合的形式。

类加载器的双亲委托模型的好处:

  • 可以确保 Java 核心库的类型安全。所有的 Java 应用都至少会引用 java.lang.Objec t类,也就是说在运行期,java.lang.Object 这个类会被加载到 Java 虚拟机中。如果这个加载过程是由自定义的类加载器完成的,那么很可能就会在 JVM 中存在多版本的java.lang.Object 类,而且这些类还是不兼容的,相互不可见的(因为命名空间)。借助于双亲委托机制,Java 核心类库中的类的加载工作都是由启动类加载器来统一完成,从而确保了 Java 应用所使用的都是同一版本的 Java 核心类库。
  • 可以确保 Java 核心类库所提供的类不会被自定义的类所替代。
  • 不同的类加载器可以为相同名称(全限定类名)的类创建额外的命名空间。相同名称的类可以并存在Java虚拟机中,只需要使用不同的类加载器来加载它们即可。不同类加载器所加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个又一个相互隔离的 Java 类空间,这类技术在很多框架中都得到了实际应用。

线程上下文类加载器

首先我们看一下SPI的概念

SPI(Service Provider Interface)

对于 SPI,大多数开发人员可能不熟悉,因为这个是针对厂商或者插件的。在 java.util.ServiceLoader 的文档里有比较详细的介绍。简单的总结下 java spi 机制的思想。我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案,xml 解析模块、jdbc 模块的方案等。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。

java spi 的具体约定为:当服务的提供者,提供了服务接口的一种实现之后,在 jar 包的 META-INF/services/ 目录里同时创建一个以服务接口命名的文件,该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该 jar 包 META-INF/services/ 里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。 基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里指定。jdk 提供服务实现查找的一个工具类:java.util.ServiceLoader。

线程上下文类加载器

线程上下文类加载器从 JDK1.2 开始引入,类 Thread 中的 getContextClassLoader() 与setContextClassLoader(ClassLoader loader) 分别用来获取和设置上下文类加载器。

如果没有通过 setContextClassLoader(ClassLoader loader) 进行设置的话,线程将继承其父线程的上下文类加载器。

Java 应用运行时的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过该类加载器来加载类与资源。

线程上下文类加载器的重要性:

父 ClassLoader 可以使用当前线程 Thread.currentThread().getContextLoader() 所指定 ClassLoader 加载的类。这就改变了父类加载器不能使用子类加载器或是其他没有直接父子关系的类加载器加载的类的情况,即改变了双亲委托模型。

在双亲委托模型下,类的加载是由下至上的,即下层的类加载器会委托上层进行加载。但是对于 SPI 来说,有些接口是 Java 核心库提供的,而 Java 核心库是由启动类加载器来加载的,而这些接口的实现却来自于不同的jar包(厂商提供),Java 的启动类加载器是不会加载其他来源的 jar 包,这样传统的双亲委托模型就无法满足 SPI 的要求。而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载。

线程上下文类加载器的一般使用模式(获取 - 使用 - 还原)

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try {
    Thread.currentThread().setContextClassLoader(targetClassLoader);
    myMethod();
} finally {
    Thread.currentThread().setContextClassLoader(classLoader);
}
  • 24
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值