JVM—虚拟机类加载器

参考资料:深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明

1. 类加载器

JVM设计团队有意把类加载阶段中的 "通过一个类的全限定名来获取该类的二进制字节流" 这个动作放到JVM外部实现,这个动作的代码称为类加载器。

1.1 类与类加载器

类加载器虽然只作用于实现类的加载动作,但是在Java程序中起到的作用远超类加载阶段。

  • 对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立在其 Java虚拟机 中的唯一性,也就是比较两个类是否"相等",只有两个类是同一个类加载器加载的前提下才有意义。

  • 同一个Class文件,被同一个Java虚拟机加载,只要类加载器不同,那么这两个类必然不相等。

这里的"相等",包括Class对象的equals、isAssignableFrom、isInstance方法的返回结果。也包括了instanceof关键字的对象关系判定。

java 复制代码
package JvmTest;

import java.io.IOException;
import java.io.InputStream;

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {


        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String filename = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(filename);

                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);

                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };

        //loadClass参数代表类的全限定名
        Object obj = myLoader.loadClass("JvmTest.ClassLoaderTest");
        System.out.println(obj.getClass());
        System.out.println(obj instanceof ClassLoaderTest);

    }
}
// 运行结果:
class java.lang.Class
false

1.2 双亲委派模型

从JVM角度来讲只有两种不同的类加载器:一种是启动类加载器,另一个是其他的所有类加载器(都是由Java语言实现,并且全部继承自抽象类ClassLoader

1.2.1 三层类加载器

1.2.1.1 启动类加载器
  • 负责加载 <JAVA_HOME>\lib 目录的 .jar 文件

  • 负责加载JRE核心库,这些库是本地代码实现,无法在java中访问到。

  • **启动类加载器**无法被java程序直接引用,在自定义类加载器时,将需要加载请求委派给启动类加载器处理。

1.2.1.2 扩展类加载器
  • 负责加载 <JAVA_HOME>\lib/ext 目录下的所有类库,这些类库提供了对核心类库的扩展。

  • 扩展类加载器由java实现的,因此可以直接在程序中使用扩展类加载器来加载Class文件。

1.2.1.3 应用程序类加载器
  • 应用程序类加载器用于加载应用程序的类和资源(通常从CLASSPATH中指定的目录和JAR文件中加载)。

  • 负责加载用户路径上的所有类库,同样可以直接在代码中使用这个类加载器。

  • 如果没有显示的定义自己的类加载器,那么这个就是应用程序默认的类加载器

1.2.2 双亲委派模型

如图展示的各类加载器之间的层次关系被称为类加载器的"双亲委派模型",双亲委派模型除了要求有顶层的启动类加载器,并且其余的类都有自己的父类加载器。

不过各类加载器之间不是通过**继承** 关系实现,而是通过**组合**的关系实现复用。

双亲委派的优点

  • 使用双亲委派模型组织类加载器之间的关系,好处在于Java中的类也享受到了这种优先的层级关系,无论加载哪个类最终都是最顶层启动类加载器加载。

  • 可以保证各种类加载器环境都是同一个类。

  • 如果没有这种机制,例如有一个系统Object和用户编写的Object,会出现重名导致编译通过,但永远无法加载运行

1.2.2.1 双亲委派模型的实现
java 复制代码
protected synchronized Class<?> loadClass(String name, boolean resolve) throws
        ClassNotFoundException {
    // 首先,检查请求的类是否已经被加载过了
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                // 传递给父类
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            // 如果父类加载器抛出 ClassNotFoundException
            // 说明父类加载器无法完成加载请求
        }
        if (c == null) {
            // 在父类加载器无法加载时
            // 再调用本身的 findClass 方法来进行类加载
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}

先检查请求加载的类型是否已经被加载过,若没有则调用父加载器的 loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。假如父类加载器加载失败,抛出ClassNotFoundException 异常的话,才调用自己的findClass()方法尝试进行加载。

1.3 破坏双亲委派模型

直到Java模块化的出现之前,双亲委派继承出现过3次较大规模的被"破坏"。

第一次

  • 父类loadClass可能被覆盖

在JDK1.2之前没有双亲委派模型,在此之后为了兼容以前的代码,无法避免loadClass()被子类覆盖的可能,增加了新的方法findClass()来完成加载,引导用户重写这个方法而不是在loadClass中编写代码。

第二次

  • 模型缺陷,双亲委派很好的解决了各类加载器之间的协作时基础类型的一致性问题(越基础的类使用越上层类加载器完成),但是基础类经常被用户继承,调用Api

  • 如果基础类型回调用户代码就会出现问题。

Java设计团队设计了一个线程上下文类加载器,这个类加载器可以通过Thread类的setContextClassLoader()来设置。

它使得 父类 加载器可以去请求子类加载器完成类加载行为,实际上打破了双亲委派的层次结构。

第三次

  • 由于用户追求程序的动态性导致的,企图实现类似于热插拔的效果,在不重启设备的情况下。

使用OSGi实现模块化热部署,它自定义了类加载机制的实现,每个应用模块(Bundle)都有一个自己的类加载器,当需要更换Bundle时,就连同类加载器一起换掉实现代码的热替换。

但是,OSGi不再使用双亲委派机制,而是发展成一种更加复杂的网状结构。

1.4 Java模块化系统

JDK9引入了模块化系统,为了实现模块化的关键目标---可配置的封装隔离机制。Java对类加载机制也做出了调整。

1.4.1 什么是可配置的封装隔离机制

是一种软件设计模式,它通过将系统中的相关组件(类、函数、变量等)封装到一个独立的容器中,实现组件之间的逻辑隔离。 这个容器可以被动态地配置和修改,实现不同的功能需求。

1.4.2 可配置的封装隔离机制解决的问题

  1. 解决了JDK9之前基于类路径(ClassPath)来查找依赖的可靠性问题。启用了模块化进行封装,模块声明对其他模块的依赖,使得JVM在启动时就能验证应用程序的依赖关系是否完备。

  2. 解决了原来路径上跨JAR文件的public类型的可访问性问题,JDK9的public不意味着程序的所有代码都可以访问,模块提供了更加精细化的访问(必须声明哪些public类型可以被哪些模块访问)。

1.4.3 模块下的类加载器

为了保证兼容性,JDK9没有动摇三层类加载器架构和双亲委派模型,为了保证模块化正常运行,模块化的类加载器发生了一些变化。

  • 首先扩展类加载器平台类加载器取代。

由于整个JDK都基于模块化进行构建,其中Java类库天然满足可扩展的需求,自然不需要扩展类加载器。

  • 平台类加载器和应用程序类加载器继承关系发生变化。
  1. 都不再派生自java.net.URLClassLoader,现在三大加载器全部继承自BuiltinCLassLoader

  2. BuiltinCLassLoader中实现了新模块架构下类如何从模块中加载的逻辑。

  3. 即使现在启动类加载器有了BuiltinCLassLoader这样的java类,但是获取启动类加载器也会返回null代替

1.4.3.1 新的双亲委派机制
  • 当平台以及应用程序类加载器收到类加载请求,在委派给父类之前不会立即委派。

  • 而是先判断类是否可以归属到某个系统模块,如果存在归属关系则优先委派给负责该模块的加载器。

  • 这算是双亲委派机制的第四次破坏。

相关推荐
坊钰23 分钟前
【Java 数据结构】移除链表元素
java·开发语言·数据结构·学习·链表
chenziang128 分钟前
leetcode hot100 LRU缓存
java·开发语言
会说法语的猪33 分钟前
springboot实现图片上传、下载功能
java·spring boot·后端
码农老起34 分钟前
IntelliJ IDEA 基本使用教程及Spring Boot项目搭建实战
java·ide·intellij-idea
m0_7482398338 分钟前
基于web的音乐网站(Java+SpringBoot+Mysql)
java·前端·spring boot
时雨h43 分钟前
RuoYi-ue前端分离版部署流程
java·开发语言·前端
麒麟而非淇淋1 小时前
Day13 苍穹外卖项目 工作台功能实现、Apache POI、导出数据到Excel表格
java
小爬虫程序猿1 小时前
利用Java爬虫获取速卖通(AliExpress)商品详情的详细指南
java·开发语言·爬虫
Java编程乐园1 小时前
Java中以某字符串开头且忽略大小写字母如何实现【正则表达式(Regex)】
java·正则表达式