JVM(七):类加载器模型

回顾

上一篇已经研究了整个类的加载过程,从加载到最后面的初始化,在类加载过程中,起到比较关键作用的是类加载器,下面就来认识一下这个类加载器是做什么的

类加载器:ClassLoader

类加载器的作用就是实现可以通过一个类的全限定名来获取描述该类的二进制字节流的动作

类加载器与类

虽然在类加载的过程中,类加载器仅仅用于实现类的加载动作,但它在Java程序中起到的作用却远远超过类加载阶段

对于任意一个类,都必须由类本身和加载它的类加载器一起共同确立其在Java虚拟机中的唯一性(也就是说,虚拟机只有通过类和类加载器才能够唯一去确定类),每一个类加载器都拥有一个独立的类名称空间。

说得更通俗一点就是比较两个类是否相等,首先要确定加载这两个类的类加载器是不是一样的,如果类是由不一样的类加载器来加载的,即使两个类是来自同一个Class文件,被同一个虚拟机加载,都是被判定为不相等的,这里的相等关系是包括equals方法的返回结果、Instanceof的返回结果、isAssignableFrom的返回结果还有isInstance方法的返回结果

拓展:isAssignableFrom是Class实例里面的一个方法,用来判断对象所表示的类或接口与参数所表示的类或接口是不是相同的;isInstance也是Class实例里面的一个方法,用来判断指定obj对象是不是使用该类来进行实例的

这里的关键在于虚拟机是通过类和类加载器去唯一确定类的,假如使用不同的类加载去加载相同全限定名称的类,该类也会被判定成不相同的

类加载器模型

从虚拟机的角度出发,只存在两种类加载器,一种是启动类加载器(BootStrap ClassLoader),对于HotSpot来说,启动类加载器是由C++语言来实现的,但可能对于其他厂家的虚拟机,比如MRP、Maxint等,整个虚拟机都是纯Java语言编写的;而另外一种则是其他类加载器,这些类加载器都是使用Java代码来进行实现的,独立于Java虚拟机,并且全都继承自抽象类java.lang.ClassLoader

但是从开发人员的角度来看的话,就应该划分得更加细致一些,从架构上来看,Java将类加载器划分成了两类

  • 三层类加载器
  • 双亲委派的类加载器

下面就看看这两种的类加载器是怎样的

常用的类加载器

首先我们要先认识一下,虚拟机常用的类加载器

绝大多数的Java程序都会使用到以下3个系统提供的类加载来进行加载

  • 启动类加载器
  • 扩展类加载器
  • 应用程序类加载器
启动类加载器

启动类加载器是负责加载存放在java_home下的lib目录或者-Xbootclasspath参数所指定的路径所存放的jar,并且是Java虚拟机能够识别的类库,启动类加载器会将这些类库加载到虚拟机的内存中等待使用

启动类加载器是无法被应用程序直接使用的,如果我们在自定义类加载器的时候,如果需要把加载请求委派给引导类加载器去处理,最好的做法其实就是让自定义类加载的时候为null,就是不去定义类加载器。。。。。。感觉理解起来很绕口

直接看代码吧,下面是Class实例去获取自身类加载器的方法

在这里插入图片描述
从代码上看不出什么东西,所以我们从注释上去理解

在这里插入图片描述
注释上说明,该方法返回该class所使用的ClassLoader,一些类加载器实现类可能会使用null来表示bootstrap classLoader(启动类加载器),所以返回了getClassLoader方法返回为null,加旧代表使用的是启动类加载器

扩展类加载器

扩展类加载器是使用Java代码来实现的,并且是Launcher下面的一个内部类

在这里插入图片描述
ExtClassLoader负责加载lib目录下的ext路径的,并且从名字可以看出,这个是对于Java类库的一种扩展机制,因为该ExtClassLoader是使用Java来实现的,所以我们可以直接在应用程序上使用ExtClassLoader来加载Class文件

应用类加载器

这个应用类加载器与扩展类加载器一样,同样是使用Java语言来实现,同样是Launch类下面的内部类,同样也可以直接使用,名称为AppClassLoader,同时AppClassLoader被称为系统类加载器,这是因为ClassLoader.getSystemClassLoader的返回值就是它

在这里插入图片描述
应用类加载器负责加载用户类路径上所有的类库

这里先说明这个用户类路径,经过输出打印的结果如下

在这里插入图片描述

D:\Program Files\Java\jdk1.8.0_152\jre\lib\charsets.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\deploy.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\access-bridge-64.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\cldrdata.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\dnsns.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\jaccess.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\jfxrt.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\localedata.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\nashorn.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunec.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunjce_provider.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunmscapi.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunpkcs11.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\zipfs.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\javaws.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\jce.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\jfr.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\jfxswt.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\jsse.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\management-agent.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\plugin.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\resources.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\rt.jar
D:\代码\leetcode\out\production\leetcode

可以看到类路径代表着lib、还有lib\ext下的,最重要的还有out文件夹下的,要知道out文件夹里面就是java项目里面编译出来的所有Class文件,所以应用类加载器主要就是负责加载我们自己写出来的Java代码编译出来的Class文件

双亲委派模型

下面我们直接看这个双亲委派模型是怎样的一个模型

在这里插入图片描述
可以看到,这个双亲委派模型跟继承关系十分类似,但其实两者并不是同种概念,要进行区分开来

在这里插入图片描述
我们可以看到,AppClassLoader并没有继承ExtClassLoader

但我们通过AppClassLoader的getParent方法去获取上层加载器并打印出来的时候,可以知道AppClassLoader的上层加载器就是ExtClassLoader

在这里插入图片描述
那么Java到底是如何实现这个双亲委派模型的呢???

双亲委派模型要求除了顶层的启动类服务器外,其余的类加载器都应有自己的父类加载器,不过这里的父类加载器并不是以继承的关系来实现的,而是使用组合的形式来实现的,从而复用父类的代码(这也是为什么在Launcher下没有看到AppClassLoader继承了ExtClassLoader),组合的形式也很简单,通过父加载器是子加载器的成员属性,这样就是组合了

但双亲委派模型只是推荐的一种形式,也就是说通常是这种模型,但其实双亲委派模型并不是一个具有强制约束力的模型,仅仅只是Java设计者们推荐给开发者的一种类加载器实现的最佳实践

双亲委派模型的工作过程

都说是双亲委派的,肯定要先考虑双亲呀

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会去加载这个类,而是把加载类的请求委派到父类加载器去完成,每一个层次的类加载器也是如此,如果父类加载器也有自己的父类加载器,那么也不会自己去加载,而是把加载类的请求委派到其父类加载器去完成,依次类推,导致的结果就是所有的加载请求的最终都会被传送到最顶层的启动类加载器中,只有当父类的加载器反馈自己无法完成这个加载请求时,子类加载器才会尝试自己去完成

那使用双亲委派模型有什么好处呢???

第一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系,正是这种优先级的层次关系保证了类加载出来都是一样的,前面提到过,类和其类加载器才能确定该类的唯一性,假如使用不同的类加载器去加载类,那么即使Class文件 相同,加载出来的结果也不一样,为了避免这种唯一性带来的不利,所以要给类加载器提供一种优先级层次,比如说,使用一个自定义的类加载去加载java.lang.Object,由于Object是由启动类加载器进行加载的(Object位于rt.jar中),所以根据双亲委派模型,最终不论哪种自定义的类加载器都会委派到启动类加载器进行加载,保证了Object类一定是由启动类加载器加载的,形成了一种绑定关系,那么Obejct类使用不同的自定义加载器去加载都能得到相同的结果

如果没有双亲委派模型,用户只要定义不同的自定义类加载器,去加载同一个全限定名的类,那么都会产生不同的结果

双亲委派模型对于保证Java程序的稳定运行极为重要,但它的实现却极为简单,下面就来看看这部分的代码,位于ClassLoader的loadClass方法上

  protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            //检验是否已经被加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                //如果没被加载,开始加载动作
                long t0 = System.nanoTime();
                try {
                    //判断有没有父加载器
                    //前面提到过,是使用组合的方式来实现双亲委派模型的
                    if (parent != null) {
                        //如果有父加载器,递归执行父加载器的loadClass方法
                        c = parent.loadClass(name, false);
                    } else {
                        //如果没有父类加载器
                        //代表是要由启动类加载器进行加载
                        //前面也提到过,如果要将类的加载委托给启动类加载器进行
                        //就将自定义的启动类的parent设置为Null
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
				//如果上面的父加载器都加载失败
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    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方法
    • 没有父类加载器,说明要委托给启动类加载器来进行加载
  • 如果父类加载器无法加载,返回依然为null,就会交由本类加载器来进行加载

破坏双亲委派模型

前面提到过,双亲委派模型不是一个具有强制约束性的模型,在Java世界中大部分的类加载器都遵循这个模型,但也有例外的情况,在此之前已经出现过几次大规模的破坏双亲委派模型情况了

第一次破坏双亲委派模型

第一次破快双亲委派模型是在双亲委派模型还没创建出来的时候,听起来比较绕,其实也不难理解,之所以要创建双亲委派模型,是因为之前的版本不是双亲委派模型嘛,之前不是双亲委派模型那么对应的就是被破坏的双亲委派模型嘛,这波是顶级理解。。。。。。。

第一次破坏双亲委派模型,是因为远古时期(JDK1.2之前)的JAVA已经有ClassLoader这个概念了,并且已经在用了,在此之前,用户去自定义类加载器是通过重写loadClass方法来决定的,这是因为虚拟机在进行类加载的时候会调用类加载器的私有方法,也就是loadClassInternal,而这个方法的唯一逻辑就是去调用自己的loadClass

在这里插入图片描述
双亲委派的机制肯定是包含在loadClass里面的,如果把loadClass给重写了,那么怎么进行双亲委派呢?(这就是破坏双亲委派的问题,只要重写了loadClass,双亲委派模型就会被破坏掉)

所以后面针对这个破坏,Java设计者们再引入双亲委派模型时不得不做出一些妥协了,首先依然要在loadClass里实现双亲委派,然后将本类加载器加载类的细节放在findClass方法里,通过更新版本然后引导用户编写的类加载逻辑时尽量重写findClass方法,而不是去重写loadClass方法,这样既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器是符合双亲委派机制的(当然,这个前提是用户听从引导去做)

第二次破坏双亲委派模型

第二次破坏双亲委派模型是由双亲委派模型自身的缺陷导致的

有了双亲委派,就可以很好地解决了各个类加载器协作时对于基础类型的一致性问题,各个类加载器去加载基础类型的时候,最终都会委派给同一个上层的类加载器,比如说将Obejct都委派给了启动类加载器

回顾之前我们学习的类加载过程,在解析阶段会对类或接口进行解析,有一个很重要的概念就是,对于父类还是接口的解析都是交由当前子类的类加载器来完成的,也就是说对于子类、父类和接口,都是使用同一个类加载器来完成,这就是问题的所在

一般情况下,我们自己的类都可能会继承一些基础的类,比如Object类,而我们自己的类使用的是应用类加载器,而应用类加载器是支持Object类的加载的(向上委派到启动类扩展器的加载),但有时候如果我们在基础的类中去使用我们写的类呢?比如原生的JDBC,Java本来就支持JDBC,而且JDBC的类型仅仅只是一个接口,具体的实现完全是由导入的依赖决定的,如果你导入的是MySQL-connect,那就可以连接MySQL,这就是很典型的一个基础的类或接口使用我们写的类(基础的类是原生JDBC,我们写的类是MySQL-Connect-JDBC),那这就出现大问题了,因为会先加载基础的类,也就是原生的JDBC,此时使用的是启动类加载器,但到后面去加载引入的MySQL-Connect-JDBC时,需要使用的是应用类加载器,但启动类加载器是回不去应用类加载器的(只能应用类加载器一层一层往上找可以找到启动类加载器),而且启动类加载器肯定不支持自己写的代码,所以完蛋了。。。。

当然不仅JDBC,还有JNDI服务也是如此

那么要如何解决这个问题呢???

问题的根本原因就在双亲委派模型仅仅只支持从下往上找,不支持从上往下找,说白了每个加载器只有Parent,但没有Son

为了解决这个问题

Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(ContextClassLoader)

在这里插入图片描述

说白了就是给线程设置了一个类加载器,也就是在Thread里面会有一个ClassLoader

如果创建线程时没有设置,它将会考虑从父线程中继承一个,如果在应用程序的全局范围内都没有设置(没有一个线程设置过这个ClassLoader),那这个类加载器默认就是应用程序类加载器,所以一开始,这个Thread ClassLoader肯定是一个应用程序类加载器

每个线程拥有这个Thread ClassLoader之后,就可以针对那些特别的服务了,只要当前的加载器处理不了,就使用Thread ClassLoader进行加载,而Thread ClassLoader本身是一个应用程序类加载器,肯定支持所有的加载的

这种行为达到了目的,并且打通了双亲委派的层次结构,来逆向使用类加载器,不过这也违背了双亲委派模型的一般性原则,也就是破坏了双亲委派模型

第三次破坏双亲委派模型

第三次破坏双亲委派模型是由于用户对程序动态性的追求而导致的,这里所说的动态性跟热部署差不多,说白了就是Java程序希望不用重启程序也能够进行代码更新或者替换

而热部署是使用OSGI来实现的,OSGI实现模块化热部署的关键在于它自定义的类加载器机制的实现,一个大程序分成了几个模块,而几个模块都有自己的类加载器,当需要对一个模块进行更换、更新时,就把整个模块,包括模块的类加载器一起进行更换,那么此时类加载器不再是双亲委派模型推荐的树状结构了,而是进一步发展为更加负载的网状结构了,当收到类加载请求时,OSGI将按照下面的顺序进行类搜索

  1. 将以java.*开头的类,委派给父类加载器进行加载
  2. 将委派列表名单内的类,委派给父类加载器加载
  3. 将Import列表中的类,委派给Export这个类的模块的类加载器进行加载
  4. 。。。。。。

还有其他规则就不一一展示了,从第三条规则就可以看出OSGI已经打破了双亲委派模型了,对于Import列表中的类,不是委派给父类加载器,而是委派给了对应模块的类加载器

破坏双亲委派模型并不一定是不好的行为,只要有足够充分的理由和正确的目的,打破就等于是创新!!!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值