类的加载器上篇

概述

类加载器是 JVM 执行类加载机制的前提

作用
ClassLoader 是 Java 的核心组件,所有的 class 都是由 ClassLoader 进行加载的,ClassLoader 负责通过各种方式将 class 信息的二进制数据流读入 JVM 内部,转换为一个与目标类对应的 java.lang.Class 对象实例。然后交给 Java 虚拟机尽心链接、初始化等操作。因此,ClassLoader 在整个装载阶段,只能影响到类的加载,而无法通过 ClassLoader 去改变类的链接和初始化行为。至于它是否可以运行,则由 Execution Engine 决定
在这里插入图片描述
类加载器最早出现在 Java 1.0 版本中,那个时候只是单纯地为了满足 Java Applet 应用而被研发出来,但如今类加载器却在 OSGI、字节码加解密领域大放异彩。这主要归功于 Java 虚拟机的设计者们当初在设计类加载器的时候,并没有考虑将它绑定在 JVM 内部,这样做的好处就是能够更加灵活和动态地执行类加载操作

类加载的分类

类的加载分类:显式加载 vs 隐式加载
class 文件的显式加载与隐式加载的方式是指 JVM 加载 Ccass 文件到内存的方式

  • 显式加载指的是在代码中通过调用 ClassLoader 加载 Class 对象,如直接使用 Class.forName(name) 或 this.getClass().getClassLoader().loadClass() 加载 Class 对象
  • 隐式加载则是不直接在代码中调用 ClassLoader 的方法加载 Class 对象,而是通过虚拟机自动加载到内存中,如在加载某个类的 Class 文件时,该类的 Class 文件中引用了另外一个类的对象,此时额外引用的类将通过 JVM 自动加载到内存中

在日常开发中以上两种方式一般会混合使用

类加载器的必要性
一般情况下,Java 开发人员并不需要在程序中显式地使用类加载器,但是了解类加载器的加载机制却显得至关重要。从以下几个方面说:

  • 避免在开发中遇到 java.lang.ClassNotFoundException 异常或 java.lang.NoClassDeFoundError 异常时手足无措。只有了解类加载器的加载机制才能够在出现异常的时候快速地根据错误异常日志定位问题和解决问题
  • 需要支持类的动态加载或需要对编译后的字节码文件进行加解密操作时,就需要与类加载器打交道了
  • 开发人员可以在程序中编写自定义类加载器来重新定义类的加载规则,以便实现一些自定义的处理逻辑

命名空间

概念

  • 每个类加载器都有各自的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。
  • 在同一个命名空间中,不会出现全类名相同的两个类。
  • 在不同的命名空间中,有可能出现全类名相同的两个类。
    在这里插入图片描述

不同类加载器的命名空间关系

  • 同一个命名空间中的类是相互可见的。
  • 子加载器的命名空间包含了所有父加载器的命名空间,因此,子加载器中加载的类能看到所有父加载器中加载的类,而父加载器加载的类看不到子加载器加载的类。
  • 如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类互不可见

每个类加载器都有各自的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。
在同一个命名空间中,不会出现全类名相同的两个类。
在不同的命名空间中,有可能出现全类名相同的两个类。

命名空间是类加载器中一个很重要的概念,对于只要学过java的人都知道java万物皆对象,在java中即使是一个“.class”文件,通过类加载器加载到虚拟机内存,那么在内存中会生成一个对应的Class对象

那么问题又来了,你应该听说过,一个类在内存中只能有一个Class对象,那么真的是这样吗?没有任何前提吗?接下类我们就来详细的分析一下,为什么那么多人说同一个类在内存中有且只有一个Class对象?真的是这样吗?

首先先来介绍一下类加载器,只有了解了类加载器的概念你才能理解我接下来说的。

在java中一共有三种类加载器,如果也可以说是四种,因为还有一种是我们的自定义类加载器,需要我们自己实现,

  1. 根类加载器也叫启动类加载器,我们的rt.jar包就是根类加载器加载的,如果具体的根类加载负责的classPath可以通过System.getProperty("sun.boot.class.path") 查看根类加载器负责加载的路径
  2. 扩展类加载器,通过System.getProperty("java.ext.dirs") 即可查看
  3. 应用加载器,也叫系统类加载器,
  4. System.getProperty("java.class.path") 即可查看
    自定义类加载器

接下来我们来理解一下类加载器的双亲委派机制

当一个类加载器尝试加载某一个类之前会先委派给它的父类加载器,以此类推,如果父类加载器加载不了才会自己加载,如果自己也加载不了,这时候就会抛出异常

平时我们自己写的类都是由系统类加载器负责加载,所以平时我们写的类信息都保存在系统类加载器的命名空间中

命名空间:

命名空间是由该类加载器以及其父类加载器所构成的,其中父类加载器加载的类对其子类可见,但是反过来子类加载的类对父类不可见,同一个命名空间中一定不会出现同一个类(全限定名一模一样的类)多个Class对象,换句话说就是在同一命名空间中只能存在一个Class对象,所以当你听别人说在内存中同一类的Class对象只有一个时其实指的是同一命名空间中,当然也不排除他压根就不知道这个概念。

到这里你应该知道,同一命名空间一个类的Class对象只有一个,那么不同的命名空间呢?看来你能想到这个问题以及很厉害了

接下来我们来看一个异常
在这里插入图片描述
可能你一眼就看出来了,这不就是一个类型转换异常吗?是的!没错!但你看看我圈中的两条信息,发现没有?明明是同一类,而java却告诉我不能转换?这是什么鬼?

当然只看这个异常你是看不懂的,接下类我把代码贴出来,并详细的分析一下

在这里插入图片描述
这是一个自定义的类加载器,path为成员属性,来指定这个类加载器负责加载的classPath

extName为扩展的名是一个常量指定为“.class”,然后通过defineClass来返回一个Class对象

这是Studnt类

在这里插入图片描述
这是测试类

在这里插入图片描述
我们先不讨论异常的事,我们先把这段代码给说清楚

实例化了两个自定义类加载器 loader1和loader2,分别将他们的classPath指定到我电脑的G盘下,

然后通过loader1和loader2分别加载Student类,正常来说这是没有任何问题的。而且还会输出true,先来讲讲为什么没有问题,只有理解了为什么不会出现问题,才能理解为什么出问题

按照类加载器的层级关系 自定义类加载器->系统类加载器->扩展类加载器->根类加载器 是按照这种层级关系来的,这是的层级关系并不是通过继承体现的,而是在自类加载器的内部有个成员属性保存了父类加载器

然后我们知道类加载器是有双亲委派机制的,那么loader1加载student类之前一定会去让它的父类,也就是系统类加载器去加载,系统类加载器然后又让它的父类加载,,以此类推,还是由最后系统类加载器加载,因为它的父类都加载不了,

那么loader2去加载的时候也会按照上面那种流程,但是会先判断这个类是否已经加载过了,如果加载过了就直接返回这个类的Class对象,很显然Student已经被系统类加载器加载过了,所以clazz1和clazz2都代表同一个对象,他们肯定是相同的,然后调用方法本来传入的就是Student对象 ,向下转型也是没问题的,那么我刚刚那个异常是怎么导致的呢?

细心的读者应该会发现,在那个异常信息的上面还打印了一个false。

什么?false?他们不是同一个Class对象吗?接下来看我动了哪些手脚

通过System.property("java.class.path")获取到系统类加载器的classPath当然使用Idea的话编译后的代码是存放在out这个目录的,用idea的都知道,我这里只是因为idea上面有其他的项目所以才使用Eclipse写一下,Eclipse会存放在一个bin目录下,如果实在不知道可以通过上面那行代码确定一下
在这里插入图片描述
有没有发现什么问题?我把Student的class字节码文件删除了,
在这里插入图片描述

然后在我的G盘保存了一份,然后我通过我自己的类加载器来加载这个Student肯定是能加载的,但是这样的话系统类加载是无法加载的,因为当前字节码文件没有在系统类加载器的classPath中,所以只能由我们的自定义类加载器加载,然后我们通过loader1和loader2各自加载一个。但是此时,loader1和loader2是没有任何关系的,他们加载前只会去找它的父类,父类加载不了只能自己加载,而loader1和loader2没有任何关系,他们加载的类只能它们的子类可见,故他们各自加载了一个Student类,这就加载了两个Class对象,而且他们存放在不同的命名空间中,不同的命名空间中的对象是互不可见的,到这里你应该明白为什么是false了,但是类型转换异常又是怎么出来的呢?其实这个也很好理解,连Class对象都不同,那又怎么转换呢?而且他们还互不可见

摘自https://blog.csdn.net/yuge1123/article/details/99945983

作用
在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本

类加载机制的基本特征

通常类加载机制有三个基本特征:

  • 双亲委派模型。但不是所有类加载都遵守这个模型,有的时候,启动类加载器所加载的类型,是可能要加载用户代码的,比如 JDK 内部的 ServiceProvider/ServiceLoader 机制,用户可以在标准 API 框架上,提供自己的实现,JDK 也需要提供些默认的参考实现。例如,Java 中 JNDI、JDBC、文件系统、Cipher 等很多方面,都是利用的这种机制,这种情况就不会用双亲委派模型去加载,而是利用所谓的上下文加载器
  • 可见性,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的。不然,因为缺少必要的隔离,我们就没有办法利用类加载器去实现容器的逻辑
  • 单一性,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,就不会在子加载器中重复加载。但是注意,类加载器"邻居"间,同一类型仍然可以被加载多次,因为相互并不可见

类的加载器分类

在这里插入图片描述

JVM 支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)

从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是 Java 虚拟机规范却没有这么定义,而是将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器。无论类加载器的类型如何划分,在程序中我们最常见的类加载器结构主要是如下情况:
在这里插入图片描述

在这里插入图片描述

Bootstrap ClassLoader

Bootstrap ClassLoader为根类加载器,负责加载java的核心类库。根加载器不是ClassLoader的子类,是有C++实现的。

public class BootstrapTest {
    public static void main(String[] args) {
        //获取根类加载器所加载的全部URL数组
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        Arrays.stream(urLs).forEach(System.out::println);
    }
}
//输出结果
//file:/C:/SorftwareInstall/java/jdk/jre/lib/resources.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/rt.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/sunrsasign.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jsse.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jce.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/charsets.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jfr.jar
//file:/C:/SorftwareInstall/java/jdk/jre/classes

根类加载器负责加载%JAVA_HOME%/jre/lib下的jar包(以及由虚拟机参数 -Xbootclasspath 指定的类)。
在这里插入图片描述
我们将rt.jar解压,可以看到我们经常使用的类库就在这个jar包中。

Extension ClassLoader

Extension ClassLoader为扩展类加载器,负责加载%JAVA_HOME%/jre/ext或者java.ext.dirs系统熟悉指定的目录的jar包。大家可以将自己写的工具包放到这个目录下,可以方便自己使用。

System ClassLoader

System ClassLoader为系统(应用)类加载器,负责加载加载来自java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader.getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器默认都以系统类加载器作为父加载器。

类加载机制
JVM主要的类加载机制。

  1. 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖和引用的其他Class也由该类加载器负责载入,除非显示使用另一个类加载器来载入。
  2. 父类委托(双亲委派):先让父加载器试图加载该Class,只有在父加载器无法加载时该类加载器才会尝试从自己的类路径中加载该类。
  3. 缓存机制:缓存机制会将已经加载的class缓存起来,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存中不存在该Class时,系统才会读取该类的二进制数据,并将其转换为Class对象,存入缓存中。这就是为什么更改了class后,需要重启JVM才生效的原因。

注意:类加载器之间的父子关系并不是类继承上的父子关系,而是实例之间的父子关系。
在这里插入图片描述

public class ClassloaderPropTest {
    public static void main(String[] args) throws IOException {
        //获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println("系统类加载器:" + systemClassLoader);
        /*
        获取系统类加载器的加载路径——通常由CLASSPATH环境变量指定,如果操作系统没有指定
        CLASSPATH环境变量,则默认以当前路径作为系统类加载器的加载路径
         */
        Enumeration<URL> eml = systemClassLoader.getResources("");
        while (eml.hasMoreElements()) {
            System.out.println(eml.nextElement());
        }
        //获取系统类加载器的父类加载器,得到扩展类加载器
        ClassLoader extensionLoader = systemClassLoader.getParent();
        System.out.println("系统类的父加载器是扩展类加载器:" + extensionLoader);
        System.out.println("扩展类加载器的加载路径:" + System.getProperty("java.ext.dirs"));
        System.out.println("扩展类加载器的parant:" + extensionLoader.getParent());
    }
}
//输出结果
//系统类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
//file:/C:/ProjectTest/FengKuang/out/production/FengKuang/
//系统类的父加载器是扩展类加载器:sun.misc.Launcher$ExtClassLoader@1540e19d
//扩展类加载器的加载路径:C:SorftwareInstalljavajdkjrelibext;C:WINDOWSSunJavalibext
//扩展类加载器的parant:null

从输出中验证了:系统类加载器的父加载器是扩展类加载器。但输出中扩展类加载器的父加载器是null,这是因为父加载器不是java实现的,是C++实现的,所以获取不到。但扩展类加载器的父加载器是根加载器。

类加载流程图
在这里插入图片描述
图中红色部分,可以是我们自定义实现的类加载器来进行加载。

创建并使用自定义类加载器

自定义类加载分析
除了根类加载器,所有类加载器都是ClassLoader的子类。所以我们可以通过继承ClassLoader来实现自己的类加载器。

ClassLoader类有两个关键的方法:

  1. protected Class loadClass(String name, boolean resolve):name为类名,resove如果为true,在加载时解析该类。
  2. protected Class findClass(String name) :根据指定类名来查找类。

所以,如果要实现自定义类,可以重写这两个方法来实现。但推荐重写findClass方法,而不是重写loadClass方法,因为loadClass方法内部会调用findClass方法。

我们来看一下loadClass的源码

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            //第一步,先从缓存里查看是否已经加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                //第二步,判断父加载器是否为null
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                   //第三步,如果前面都没有找到,就会调用findClass方法
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                   sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

loadClass加载方法流程

  1. 判断此类是否已经加载;
  2. 如果父加载器不为null,则使用父加载器进行加载;反之,使用根加载器进行加载;
  3. 如果前面都没加载成功,则使用findClass方法进行加载。

所以,为了不影响类的加载过程,我们重写findClass方法即可简单方便的实现自定义类加载。

  1. 实现自定义类加载器
    基于以上分析,我们简单重写findClass方法进行自定义类加载。
public class Hello {
   public void test(String str){
       System.out.println(str);
   }
}

public class MyClassloader extends ClassLoader {

    /**
     * 读取文件内容
     *
     * @param fileName 文件名
     * @return
     */
    private byte[] getBytes(String fileName) throws IOException {
        File file = new File(fileName);
        long len = file.length();
        byte[] raw = new byte[(int) len];
        try (FileInputStream fin = new FileInputStream(file)) {
            //一次性读取Class文件的全部二进制数据
            int read = fin.read(raw);
            if (read != len) {
                throw new IOException("无法读取全部文件");
            }
            return raw;
        }
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = null;
        //将包路径的(.)替换为斜线(/)
        String fileStub = name.replace(".", "/");
        String classFileName = fileStub + ".class";
        File classFile = new File(classFileName);

        //如果Class文件存在,系统负责将该文件转换为Class对象
        if (classFile.exists()) {
            try {
                //将Class文件的二进制数据读入数组
                byte[] raw = getBytes(classFileName);
                //调用ClassLoader的defineClass方法将二进制数据转换为Class对象
                clazz = defineClass(name, raw, 0, raw.length);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //如果clazz为null,表明加载失败,抛出异常
        if (null == clazz) {
            throw new ClassNotFoundException(name);
        }
        return clazz;
    }

    public static void main(String[] args) throws Exception {
        String classPath = "loader.Hello";
        MyClassloader myClassloader = new MyClassloader();
        Class<?> aClass = myClassloader.loadClass(classPath);
        Method main = aClass.getMethod("test", String.class);
        System.out.println(main);
        main.invoke(aClass.newInstance(), "Hello World");
    }
}
//输出结果
//Hello World

ClassLoader还有一个重要的方法defineClass(String name, byte[] b, int off, int len)。此方法的作用是将class的二进制数组转换为Calss对象。

此例子很简单,我写了一个Hello测试类,并且编译过后放在了当前路径下(大家可以在findClass中加入判断,如果没有此文件,可以尝试查找.java文件,并进行编译得到.class文件;或者判断.java文件的最后更新时间大于.class文件最后更新时间,再进行重新编译等逻辑)。

摘自:https://zhuanlan.zhihu.com/p/108180758

对上文再进行刨析

  • 除了顶层的启动类加载器外,其余的类加载器都应当有自己的"父类"加载器
  • 不同类加载器看似是继承(Inheritance)关系,实际上是包含关系。在下层加载器中,包含着上层加载器的引用
  • 在这里插入图片描述
class ClassLoader {
  ClassLoader parent; //父类加载器

  public ClassLoader(ClassLoader parent) {
    this.parent = parent;
  }
}

class ParentClassLoader extends ClassLoader {
  public ParentClassLoader(ClassLoader parent) {
    super(parent);
  }
}

class ChildClassLoader extends ClassLoader {
  public ChildClassLoader(ClassLoader parent) {
    //parent = new ParentClassLoader();
    super(parent);
  }
}

引导类加载器

启动类加载器(引导类加载器)

  • 这个类加载使用 C/C++ 语言实现的,嵌套在 JVM 内部
  • 它用来加载 Java 的核心库(JAVA_HOME/jre/lib/rt.jar 或 sun.boot.class.path 路径下的内容)。用于提供 JVM 自身需要的类
  • 并不继承自 java.lang.ClassLoader,没有父加载器
  • 出于安全考虑,Bootstrap 启动类加载器之加载包名为 java、javax、sun 等开头的类
  • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器
  • 在这里插入图片描述

扩展类加载器

  • Java 语言编写,由 sun.misc.Launcher$ExtClassLoader 实现
  • 继承于 ClassLoader 类
  • 父类加载器为启动类加载器
  • 从 java.ext.dirs 系统属性所指定的目录中加载类库,或从 JDK 的安装目录的 jre/lib/ext 子目录下加载类库。如果用 户创建的 JAR 放在此目录下,也会自动由扩展类加载器加载

系统类加载器

应用程序类加载器(系统类加载器,AppClassLoader)

  • Java 语言编写,由 sun.misc.Launcher$AppClassLoader 实现
  • 继承于 ClassLoader 类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量 classpath 或系统属性 java.class.path 指定路径下的类库
  • 应用程序中的类加载器默认是系统类加载器
  • 它是用户自定义类加载器的默认父加载器
  • 通过 ClassLoader 的 getSystemClassLoader() 方法可以获取到该类加载器

系统类加载器

应用程序类加载器(系统类加载器,AppClassLoader)

  • Java 语言编写,由 sun.misc.Launcher$AppClassLoader 实现
  • 继承于 ClassLoader 类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量 classpath 或系统属性 java.class.path 指定路径下的类库
  • 应用程序中的类加载器默认是系统类加载器
  • 它是用户自定义类加载器的默认父加载器
  • 通过 ClassLoader 的 getSystemClassLoader() 方法可以获取到该类加载器

用户自定义类加载器

  • 在 Java 的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的。在必要时,我们还可以自定义类加载器,来定制类的加载方式
  • 体现 Java 语言强大生命力和巨大魅力的关键因素之一便是,Java 开发者可以自定义类加载器来实现类库的动态加载,加载源可以是本地的 JAR 包,也可以是网络上的远程资源
  • 通过类加载器可以实现非常绝妙的插件机制,这方面的实际应用案例不胜枚举。例如,著名的 OSGI 组件框架,再如 Eclipse 的插件机制。类加载器为应用程序提供了一种动态增加新功能的机制,这种机制无需重新打包发布应用程序就能实现
  • 同时,自定义加载器能够实现应用隔离,例如 Tomcat、Spring 等中间件和组件框架都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块。这种机制比 C/C++ 程序要好太多,想不修改 C/C++ 程序就能为其新增功能,几乎是不可能的,仅仅一个兼容性便能阻挡所有美好的设想
  • 自定义类加载器通常需要继承于 ClassLoader

借鉴于https://zhuanlan.zhihu.com/p/268637283

posted @ 2020-12-28 12:11  杰的博客#  阅读(148)  评论(0编辑  收藏  举报