深入探讨 Java 类加载器

Java类加载器可以使得 Java 类可以被动态加载到 Java 虚拟机中并执行。

类加载器的基本概念

类加载器就是用来加载Java类到Java虚拟机中。
在这里插入图片描述

ClassLoader类介绍

ClassLoader 的基本职责是根据一个类的名称,找到或者生成对应的字节码,然后根据字节码定义一个Java类,即Class类的实例。

ClassLoader 中与加载类相关的方法

方法说明
getParent()返回该类加载器的父类加载器。
loadClass(String name)加载名称为 name 的类,返回的结果是 java.lang.Class 类的实例。
findClass(String name)查找名称为 name 的类,返回的结果是 java.lang.Class 类的实例。
findLoadedClass(String name)查找名称为 name 的已经被加载过的类,返回的结果是 java.lang.Class 类的实例。
defineClass(String name, byte[] b, int off, int len)把字节数组 b 中的内容转换成 Java 类,返回的结果是 java.lang.Class 类的实例。这个方法被声明为 final 的。
resolveClass(Class<?> c)链接指定的 Java 类。

类加载器的树状结构

  • 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader 。
  • 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  • 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader() 来获取它。

触引导类加载器,其余加载器都有一个父类加载器。我们自己定义的类加载器,其父类加载器是加载此类加载器 Java 类的类加载器。
在这里插入图片描述

类加载器的代理模式

就是所说的双亲委派机制,当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

Java虚拟机是怎么判断两个类是否相同?
Java虚拟机不仅要看两个类的全路径是否相同,还要看加载该类的类加载器是否一样。即使是一个类,被不同的类加载器加载,其Class是不同的。例如Sample.class被ClassLoaderA 和ClassLoaderB加载,将加载后的两个类进行赋值,会抛出ClassCastException 。

代理莫的用处:

  • 保证了核心类库的类型安全。所有的Java应用都至少引用Object,如果Object被不同的类加载器加载到Java虚拟机中,这样会导致多个版本的Object类,而这些类之间还不兼容。
  • 不同的类加载器为相同名称的类创建了额外的名称空间。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间

加载类的过程

由于类加载器的代理模式,一个类的加载过程中,启动这个过程的类加载器和真正对这个类的加载,可能不是同一个类加载器。Java虚拟机判断两个类是否相同,是通过类定义加载器

真正完成类加载的工作是通过调用defineClass来实现,被称为类的定义加载器(defining loader)。
启动类的加载过程是通过loadClass来实现,被称为初始加载器(initiating loader)。

两种类加载器的关联之处是:一个类的加载器是他所引用的类的初始化加载器。例如Outer 引用Inner ,则Outer 的定义加载器负责启动Inner的加载过程。
loadClass() 抛出的是 java.lang.ClassNotFoundException 异常。defineClass() 抛出的是 java.lang.NoClassDefFoundError 异常。

类加载器在成功加载某个类后,会把java.lang.Class实例缓存起来。下次在调用加载的时候,类加载器会直接使用缓存中的类实例,而不会再去加载。也就是相同的全类名,一个类加载器只会加载一次。

线程上下文类加载器

通过Thread的getContextClassLoader()和setContextClassLoader()来获取和设置线程的上下文类加载器。如果没有进行设置线程的上下文类加载器的话,将继承父线程的上下文类加载器。

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

SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。
而线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。

Class.forName

  • Class.forName(String name, boolean initialize, ClassLoader loader)
  • Class.forName(String className)
    name 表示的是类的全名; initialize 表示是否初始化类;loader 表示加载时使用的类加载器。

开发自己的类加载器

文件系统类加载器

public class FileSystemClassLoader extends ClassLoader {
    private String rootDir;

    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        }
        return defineClass(name, classData, 0, classData.length);
    }

    // 读取class文件
    private byte[] getClassData(String className) {
        String path = classNameToPath(className);
        try {
            try (InputStream is = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream();) {
                byte[] buffer = new byte[1024];
                int bytesNumRead = 0;
                while ((bytesNumRead = is.read(buffer)) != -1) {
                    baos.write(buffer, 0, bytesNumRead);
                }
                return baos.toByteArray();
            }
        } catch (IOException e) {
            return null;
        }
    }

    // 类路径转文件路径
    private String classNameToPath(String className) {
        String separator = Matcher.quoteReplacement(File.separator);
        // 注意 . 需要转义 , windows 下的路径分隔符同样需要转义
        return rootDir + separator + className.replaceAll("\\.", separator) + ".class";
        // 或者使用下边的方式
        //return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
    }
}

类加载器与web容器

通常每个web应用对应一个类加载器,该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值