关于后端:深入浅出JVM八之类加载器

4次阅读

共计 10344 个字符,预计需要花费 26 分钟才能阅读完成。

前文曾经形容 Java 源文件通过前端编译器后变成字节码文件,字节码文件通过类加载器的类加载机制在 Java 虚拟机中生成 Class 对象

前文深入浅出 JVM(六)之前端编译过程与语法糖原理重点形容过编译的过程

前文深入浅出 JVM(三)之 HotSpot 虚拟机类加载机制重点形容过类加载机制的过程

本篇文章将重点聊聊类加载器,围绕类加载器深入浅出的解析类加载器的分类、品种、双亲委派模型以及从源码方面推导出咱们的论断

类加载器简介

什么是类加载器?

类加载器通过类的全限定类名进行类加载机制从而生成 Class 对象

Class 对象中蕴含该类相干类信息,通过 Class 对象可能应用反射在运行时阶段动静做一些事件

显示加载与隐式加载

类加载器有两种形式进行加载,一种是 在代码层面显示的调用 ,另一种是 当程序遇到创建对象等命令时自行判断该类是否进行过加载,未加载就先进行类加载

显示加载:显示调用 ClassLoader 加载 class 对象

隐式加载:不显示调用 ClassLoader 加载 class 对象(因为虚构机会在第一次应用到某个类时主动加载这个类)

 // 显示类加载  第 7 章虚拟机类加载机制.User 为全限定类名(包名 + 类名)
 Class.forName("第 7 章虚拟机类加载机制.User");
             
 // 隐式类加载
 new User();    

唯一性与命名空间

判断两个类是否完全相同可能并不是咱们自认为了解的那样,类在 JVM 中的唯一性须要依据类自身和加载它的类加载器

  • 唯一性

    • 所有类都由它自身和加载它的那个类在 JVM 中确定唯一性
    • 也就是说判断俩个类是否为同一个类时,如果它们的类加载器都不同那必定不是同一个类
  • 命名空间

    • 每个类加载有本人的命名空间,命名空间由所有父类加载器和该加载器所加载的类组成
    • 同一命名空间中,不存在类残缺名雷同的俩个类
    • 不同命名空间中,容许存在类残缺名雷同的俩个类(多个自定义类加载加载同一个类时,会在各个类加载器中生成对应的命名,且它们都不是同一个类)

基本特征

类加载器中有一些根本个性,比方子类加载器能够拜访父类加载器所加载的类、父类加载过的类子类不再加载、双亲委派模型等

  • 可见性

    • 子类加载器能够拜访父类加载器所加载的类 *
    • (命名空间蕴含父类加载器加载的类)
  • 单一性

    • 因为可见性,所以父类加载器加载过的类,子类加载器不会再加载
    • 同一级的自定义类加载器可能都会加载同一个类,因为它们互不可见
  • 双亲委派模型

    • 由哪个类加载器来进行类加载的一套策略,后续会具体阐明

类加载器分类

类加载器能够分成两种,一种是疏导类由非 Java 语言实现的,另一种是由 Java 语言实现的自定义类加载器

  • 疏导类加载器 (c/c++ 写的 Bootstrap ClassLoader)
  • 自定义类加载器:由 ClassLoader 类派生的类加载器类(包含扩大类,零碎类,程序员自定义加载器等)

零碎(应用程序)类加载器和扩大类加载器是 Launcher 的外部类,它们间接实现了ClassLoader

留神

平时说的零碎(应用程序)类加载器的父类加载器是扩大类加载器,而扩大类加载器的父类加载器是启动类加载器,都是 ” 逻辑 ” 上的父类加载器

实际上扩大类加载器和零碎(应用程序)类加载器间接继承的 ClassLoader 中有一个字段 parent 用来示意本人的逻辑父类加载器

类加载器品种

  • 启动(疏导)类加载器

    • Bootstrap Classloader c++ 编写,无奈间接获取
    • 加载外围库<JAVA_HOME>\lib\ 局部 jar 包
    • 不继承java.lang.ClassLoader,没有父类加载器
    • 加载扩大类加载器和应用程序类加载器,并指定为它们的父类加载器
  • 扩大类加载器

    • Extension Classloader
    • 加载扩大库<JAVA_HOME>\lib\ext*.jar
    • 间接继承java.lang.ClassLoader,父类加载器为启动类加载器
  • 应用程序 (零碎) 类加载器

    • App(System) Classloader 最罕用的加载器
    • 负责加载环境变量 classpath 或 java.class.path 指定门路下的类库,个别加载咱们程序中自定义的类
    • 间接继承java.lang.ClassLoader,父类加载器为扩大类加载器
    • 应用 ClassLoader.getSystemClassLoader() 取得
  • 自定义类加载器(实现 ClassLoader 类,重写 findClass 办法)

通过代码来演示:

 public class TestClassLoader {public static void main(String[] args) {URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
         /*
         启动类加载器能加载的 api 门路:
         file:/D:/Environment/jdk1.8.0_191/jre/lib/resources.jar
         file:/D:/Environment/jdk1.8.0_191/jre/lib/rt.jar
         file:/D:/Environment/jdk1.8.0_191/jre/lib/sunrsasign.jar
         file:/D:/Environment/jdk1.8.0_191/jre/lib/jsse.jar
         file:/D:/Environment/jdk1.8.0_191/jre/lib/jce.jar
         file:/D:/Environment/jdk1.8.0_191/jre/lib/charsets.jar
         file:/D:/Environment/jdk1.8.0_191/jre/lib/jfr.jar
         file:/D:/Environment/jdk1.8.0_191/jre/classes
         */
         System.out.println("启动类加载器能加载的 api 门路:");
         for (URL urL : urLs) {System.out.println(urL);
         } 
 ​
         /*
         扩大类加载器能加载的 api 门路:
         D:\Environment\jdk1.8.0_191\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
         */
         System.out.println("扩大类加载器能加载的 api 门路:");
         String property = System.getProperty("java.ext.dirs");
         System.out.println(property);
         
         // 加载咱们自定义类的类加载器是 AppClassLoader, 它是 Launcher 的外部类
         ClassLoader appClassLoader = TestClassLoader.class.getClassLoader();
         //sun.misc.Launcher$AppClassLoader@18b4aac2 
         System.out.println(appClassLoader);
         
         //AppClassLoader 的上一层加载器是 ExtClassLoader, 它也是 Launcher 的外部类
         ClassLoader extClassloader = appClassLoader.getParent();
         //sun.misc.Launcher$ExtClassLoader@511d50c0
         System.out.println(extClassloader);
         
         // 实际上是启动类加载器, 因为它是 c /c++ 写的, 所以显示 null
         ClassLoader bootClassloader = extClassloader.getParent();
         //null 
         System.out.println(bootClassloader);
         
         // 1 号测试:根本类型数组 的类加载器
         int[] ints = new int[10];
         //null 
         System.out.println(ints.getClass().getClassLoader());
         
         // 2 号测试:零碎提供的援用类型数组 的类加载器
         String[] strings = new String[10];
         //null 
         System.out.println(strings.getClass().getClassLoader());
         
         // 3 号测试:自定义援用类型数组 的类加载器
         TestClassLoader[] testClassLoaderArray = new TestClassLoader[10];
         //sun.misc.Launcher$AppClassLoader@18b4aac2       
         System.out.println(testClassLoaderArray.getClass().getClassLoader());
 ​
         // 4 号测试:线程上下文的类加载器
         //sun.misc.Launcher$AppClassLoader@18b4aac2
         System.out.println(Thread.currentThread().getContextClassLoader());
     }
 }

从下面能够得出结论

  1. 数组类型的类加载器是数组元素的类加载器(通过 2 号测试与 3 号测试的比照)
  2. 根本类型不须要类加载(通过 1 号测试与 3 号测试的比照)
  3. 线程上下文类加载器是零碎类加载器(通过 4 号测试)

对于类加载源码解析

用源码来解释上文论断
  • ClassLoader中的官网正文

虚拟机主动生成的一个类,治理数组,会对这个类进行类加载

对数组类类加载器是数组元素的类加载器

如果数组元素是根本类型则不会有类加载器

  • 源码解释扩大类加载器的父类是 null
  • 源码解释零碎类加载器的父类是扩大类加载器
  • 源码解释线程上下文类加载器是零碎类加载器
ClassLoader 次要办法

loadClass()

ClassLoaderloadClass办法(双亲委派模型的源码)

 public Class<?> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);
 }
                                             // 参数 resolve: 是否要解析类
 protected Class<?> loadClass(String name, boolean resolve)
             throws ClassNotFoundException
     {
        // 加锁同步 保障只加载一次
         synchronized (getClassLoadingLock(name)) {
             // 首先查看这个 class 是否曾经加载过了
             Class<?> c = findLoadedClass(name);
             if (c == null) {long t0 = System.nanoTime();
                 try {
                     // c==null 示意没有加载,如果有父类的加载器则让父类加载器加载
                     if (parent != null) {c = parent.loadClass(name, false);
                     } else {
                         // 如果父类的加载器为空 则阐明递归到 bootStrapClassloader 了
                         // 则委托给 BootStrap 加载器加载
                         //bootStrapClassloader 比拟非凡无奈通过 get 获取
                         c = findBootstrapClassOrNull(name);
                     }
                 } catch (ClassNotFoundException e) {// 父类无奈加载抛出异样}
                 // 如果父类加载器依然没有加载过,则尝试本人去加载 class
                 if (c == null) {long t1 = System.nanoTime();
                     c = findClass(name);
                     sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                     sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                     sun.misc.PerfCounter.getFindClasses().increment();
                 }
             }
             // 是否要解析
             if (resolve) {resolveClass(c);
             }
             return c;
         }
 }

先递归交给父类加载器去加载,父类加载器未加载再由本人加载

findClass()

ClassLoaderfindClass()

     protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);
     }

由子类 URLClassLoader 重写 findClass 去寻找类的规定

最初都会来到 defineClass() 办法

defineClass()

 protected final Class<?> defineClass(String name, byte[] b, int off, int len)

依据从 off 开始长度为 len 定字节数组 b 转换为 Class 实例

在自定义类加载器时,笼罩 findClass() 编写加载规定,获得要加载的类的字节码后转换为流调用 defineClass() 生成 Class 对象

resolveClass()

     protected final void resolveClass(Class<?> c) {resolveClass0(c);
     }

应用该办法能够在生成 Class 对象后,解析类(符号援用 -> 间接援用)

findLoadedClass()

     protected final Class<?> findLoadedClass(String name) {if (!checkName(name))
             return null;
         return findLoadedClass0(name);
     }

如果加载过某个类则返回 Class 对象否则返回 null

Class.forName()与 ClassLoader.loadClass()区别
  • Class.forName()

    • 传入一个类的全限定名返回一个 Class 对象
    • 将 Class 文件加载到内存时会初始化,被动援用
  • ClassLoader.loadClass()

    • 须要 class loader 对象调用
    • 通过下面的源码剖析能够晓得,双亲委派模型调用 loadClass,只是将 Class 文件加载到内存,不会初始化和解析,直到这个类第一次应用才进行初始化

双亲委派模型

双亲委派模型源码实现对应 ClassLoaderloadClass()

  • 剖析:

    1. 先查看这个类是否加载过
    2. 没有加载过,查看父类加载器是否为空,

      如果不为空,就交给父类加载器去加载(递归),

      如果为空,阐明曾经到启动类加载器了(启动类加载器不能 get 因为是 c ++ 写的)

    3. 如果父类加载器没有加载过,则递归回来本人加载
  • 举例

    1. 如果我当初本人定义一个 MyString 类,它会本人找(先在零碎类加载器中找,而后在扩大类加载器中找,最初去启动类加载器中找,启动类加载器无奈加载而后退回扩大类加载器,扩大类加载器无奈加载而后退回零碎类加载器,而后零碎类加载器就实现加载)
    2. 咱们都晓得 Java 有 java.lang.String 这个类

      那我再创立一个 java.lang.String 运行时,报错

    


    可是我明明写了 main 办法

    这是因为 ** 类装载器的沙箱平安机制 **

    很显著这里的报错是因为它找到的是启动类加载器中的 java.lang.String 而不是在应用程序类加载器中的 java.lang.String(咱们写的)

    而且外围类库的包名也是被禁止应用的
    ![image-20210425231816778.png](/img/bVdbwZL)
    




** 类装载器的加载机制:启动类加载器 -> 扩大类加载器 -> 应用程序类加载器 **

3.  如果自定义类加载器重写 `loadClass` 不应用双亲委派模型是否就可能用自定义类加载器加载外围类库了呢?

    **JDK 为外围类库提供一层爱护机制, 不论用什么类加载器最终都会调用 `defineClass()`, 该办法会执行 `preDefineClass()`,它提供对 JDK 外围类库的爱护 **
    ![image-20210517103634644.png](/img/bVdbwZM)

    
    

<!—->

  • 长处

    1. 避免反复加载同一个 class 文件
    2. 保障外围类不能被篡改
  • 毛病

    • 父类加载器无法访问子类加载器

      • 比方零碎类中提供一个接口,实现这个接口的实现类须要在零碎类加载器加载,而该接口提供动态工厂办法用于返回接口的实现类的实例,但因为启动类加载器无法访问零碎类加载器,这时动态工厂办法就无奈创立由零碎类加载器加载的实例
  • Java 虚拟机标准只是倡议应用双亲委派模型,不是肯定要应用

    • Tomcat 中是由本人先去加载,加载失败再由父类加载器去加载

自定义类加载器

  1. 继承 ClassLoader
  2. 能够覆写 loadClass 办法,也能够覆写 findClass 办法

    • 倡议覆写 findClass 办法,因为 loadClass 是双亲委派模型实现的办法,其中父类类加载器加载不到时会调用 findClass 尝试本人加载
  3. 编写好后调用 loadClass 办法来实现类加载

自定义类加载器代码

public class MyClassLoader extends ClassLoader {

    /**
     * 字节码文件门路
     */
    private final String codeClassPath;

    public MyClassLoader(String codeClassPath) {this.codeClassPath = codeClassPath;}

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 字节码文件齐全门路
        String path = codeClassPath + name + ".class";
        System.out.println(path);

        Class<?> aClass = null;
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(path));
                ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            int len = -1;
            byte[] bytes = new byte[1024];
            while ((len = bis.read(bytes)) != -1) {baos.write(bytes,0,len);
            }
            byte[] classCode = baos.toByteArray();
            // 用字节码流 创立 Class 对象
            aClass = defineClass(null, classCode, 0, classCode.length);
        } catch (IOException e) {e.printStackTrace();
        }
        return aClass;
    }
}

客户端调用自定义类加载器加载类

public class Client {public static void main(String[] args) {MyClassLoader myClassLoader = new MyClassLoader("C:\");
        try {Class<?> classLoader = myClassLoader.loadClass("HotTest");
            System.out.println("类加载器为:" + classLoader.getClassLoader().getClass().getName());
            System.out.println("父类加载器为" + classLoader.getClassLoader().getParent().getClass().getName());
        } catch (ClassNotFoundException e) {e.printStackTrace();
        }
    }
}

记得对要加载的类先进行编译

  • 留神:

    • 要加载的类不要放在父类加载器能够加载的目录下
    • 自定义类加载器父类加载器为零碎类加载器
    • JVM 所有类类加载都应用 loadClass

解释如果类加载器不同那么它们必定不是同一个类

    MyClassLoader myClassLoader1 = new MyClassLoader("D:\ 代码 \JavaVirtualMachineHotSpot\src\main\java\");
        MyClassLoader myClassLoader2 = new MyClassLoader("D:\ 代码 \JavaVirtualMachineHotSpot\src\main\java\");
        try {Class<?> aClass1 = myClassLoader1.findClass("HotTest");
            Class<?> aClass2 = myClassLoader2.findClass("HotTest");
            System.out.println(aClass1 == aClass2);//false
        } catch (ClassNotFoundException e) {e.printStackTrace();
        }
  • 长处

    • 隔离加载类 (各个中间件 jar 包中类名可能雷同, 但自定义类加载器不同)
    • 批改类加载形式
    • 扩大加载源 (能够从网络, 数据库中进行加载)
    • 避免源码透露 (Java 反编译容易, 能够编译时进行加密, 自定义类加载解码字节码)

热替换

热替换: 服务不中断,批改会立刻体现在运行的零碎上

对 Java 来说,如果一个类被类加载器加载过了,就无奈被再加载了

然而如果每次加载这个类的类加载不同,那么就能够实现热替换

还是应用下面写好的自定义类加载器

        // 测试热替换
        try {while (true){MyClassLoader myClassLoader = new MyClassLoader("D:\ 代码 \JavaVirtualMachineHotSpot\src\main\java\");
                
                Class<?> aClass = myClassLoader.findClass("HotTest");
                Method hot = aClass.getMethod("hot");
                Object instance = aClass.newInstance();
                Object invoke = hot.invoke(instance);
                TimeUnit.SECONDS.sleep(3);
            }
        } catch (Exception e){e.printStackTrace();
        }

通过反射调用 HotTest 类的 hot 办法

中途批改 hot 办法并从新编译

总结

本篇文章围绕类加载器深入浅出的解析类加载器的分类与品种、双亲委派模型、通过源码解析证实咱们的观点、最初还自定义的类加载器和阐明热替换

类加载器将字节码文件进行类加载机制生成 Class 对象从而加载到 Java 虚拟机中

类加载只会进行一次,可能显示调用执行或者在遇到创建对象的字节码命令时隐式判断是否进行过类加载

类加载器分为非 Java 语言实现的疏导类加载器和 Java 语言实现的自定义类加载器,其中 JDK 中实现了自定义类加载器中的扩大类加载器和零碎类加载器

疏导类加载器用来加载 Java 的外围类库,它的子类扩大类加载器用来加载扩大类,扩大类的子类零碎类加载器罕用于加载程序中自定义的类(这里的父子类是逻辑的,并不是代码层面的继承)

双亲委派模型让父类加载器优先进行加载,无奈加载再交给子类加载器进行加载;通过双亲委派模型和沙箱平安机制来爱护外围类库不被其余恶意代码代替

根本类型不须要类加载、数组类型的类加载器是数组元素的类加载器、线程上下文类加载器是零碎类加载器

因为类和类加载器能力确定 JVM 中的唯一性,每次加载类的类加载不同时就可能屡次进行类加载从而实现在运行时批改的热替换

最初(一键三连求求拉~)

本篇文章笔记以及案例被支出 gitee-StudyJava、github-StudyJava 感兴趣的同学能够 stat 下继续关注喔 \~

有什么问题能够在评论区交换,如果感觉菜菜写的不错,能够点赞、关注、珍藏反对一下 \~

关注菜菜,分享更多干货,公众号:菜菜的后端私房菜

本文由博客一文多发平台 OpenWrite 公布!

正文完
 0