深入浅出JVM(八)之类加载器

举报
菜菜的后端私房菜 发表于 2024/10/12 09:27:19 2024/10/12
【摘要】 深入浅出JVM(八)之类加载器前文已经描述Java源文件经过前端编译器后变成字节码文件,字节码文件通过类加载器的类加载机制在Java虚拟机中生成Class对象前文深入浅出JVM(六)之前端编译过程与语法糖原理重点描述过编译的过程前文深入浅出JVM(三)之HotSpot虚拟机类加载机制重点描述过类加载机制的过程本篇文章将重点聊聊类加载器,围绕类加载器深入浅出的解析类加载器的分类、种类、双亲委...

深入浅出JVM(八)之类加载器

前文已经描述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(我们写的)

而且核心类库的包名也是被禁止使用的

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

  1. 如果自定义类加载器重写loadClass不使用双亲委派模型是否就能够用自定义类加载器加载核心类库了呢?

    JDK为核心类库提供一层保护机制,不管用什么类加载器最终都会调用defineClass(),该方法会执行preDefineClass(),它提供对JDK核心类库的保护

  • 优点

    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中的唯一性,每次加载类的类加载不同时就能够多次进行类加载从而实现在运行时修改的热替换

最后

本篇文章将被收入JVM专栏,觉得不错感兴趣的同学可以收藏专栏哟~

觉得菜菜写的不错,可以点赞、关注支持哟~

有什么问题可以在评论区交流喔~

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。