前文曾经形容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 公布!