类加载器工作原理

作者: 风一样的行者 | 来源:发表于2018-04-08 18:20 被阅读0次

    1.首先咱们来聊一聊什么叫类加载器

    顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中,具体来说是加载.class文件到jvm内存。java源代码(.java文件)在经过java编译器编译(javac 指令)之后会生成一个或多个的.class文件,当需要生成该对象的实例时,虚拟机会去常量池查找该类是否被加载,如果没有被加载,就会调用类加载器来将.class文件中的二进制流加载到内存中。而加载二进制流的工具 (实现这一动作的代码块)就是我们所说的类加载器。

    2.类加载器的作用:

    从上面可以知道,类加载器干的活就是将对应类的.class文件中的二进制流加载到内存空间。

    注意:这里所说的加载到内存空间只是将二进制流写入内存还并没有将二进制流的存储结构解析并写入方法区,这一步操作是在  【类加载】   【验证阶段】 【文件格式验证阶段】才完成

    3.类与类加载器之间的关系

    对于任意的一个类,都需要由加载它的类加载器这个类本身一同确立其在java虚拟机中唯一性

    每一个类加载器,都拥有一个独立的类名称空间 (这也是为什么每个类的初始化只会执行一次的原因)

    通俗的来讲:比较两个类是否“相等”只有这两个类由同一个类加载器加载的前提下才有意义。否则,即使这两个类来源于同一个.class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类就必然不相等。


    那么问题来了:java如何判断两个对象是否"相等"?

    分析这个问题前先来复习两个问题,对象的比较的  ==  与  equals

    大家都知道用 ==来比较是直接判断当前比较的两个对象是不是同一个对象,也就是当前两个对象的指针指向的是不是同一片内存区域,简单来说:== 的比较方式判断的是 内存地址 也就是  是不是同一个对象。因为虚拟机中在同一时刻一个内存块只能存放一个对象,也就是说内存地址是唯一的。

    equals的比较方法在继承自Object的子类没有重写equals方法之前调用的是父类也就是Object的equals方法。而Object的equals方法实现使用的就是 == 判断两个对象是不是同一个对象。

    源码:

    public boolean equals(Object obj) {    // Object的equals方法实现

                     return ( this  ==  obj);

    }

    如果子类重写了equals方法,则调用的是子类自己的实现逻辑,我们一般把equals比较的结果当成是对象的值的比较,也就是 equals比较的是对象的  值

    hashcode与equals的关系

    Object中有个 hashcode方法,这个方法默认返回的是内存地址生成的一个 int 值(虚拟机的实现不同,可能有出入),也就是说默认的hashcode是唯一的(因为内存地址唯一)

    但是如果重写了Object的hashcode方法,那么hashcode的生成规则是由子类自己决定,与内存地址就无关了(只是与当前对象的内存地址无关了,其实质还是通过对象的某些字段的内存地址生成的int值)。

    所以对象是否相等这里要分成三种情况来讨论

    1.没有重写 对象的equals 与 hashcode 方法

    如果对象没有重写equals方法,那么比较的是两个对象的 hashcode(内存地址根据一定的规则生成),实质上也就是对象的内存地址,更通俗一点就是两个  是不是同一个对象

    此处 hashcode 不相等,像个对象必然不相等 ,反之亦然

    2.重写了 equals方法,但是没有重写hashcode方法

    如果重写了equals方法,那么我们就是根据 自己定义的equals方法 比较对象的值。由于没有重写hashcode方法,那么hashcode的生成规则依旧是根据  内存按照一定规则生成

    此处分为两种情况:

    1.如果两个对象的equals 方法为真,但他们的 hashcode不一定相同.

    2.如果两个对象的hashcode相同,那么他们是同一个对象,当然equals会为真

    3.重写了equals 与 hashcode 方法

    重写了hashcode方法,那么hashcode就是根据自己定义的规则生成,与内存地址无关了

    如果重写了equals与hashcode方法那么比较的规则就是与Object的基本一致,唯一不同的是,调用的是子类中的equals与hashcode方法

    此处如果hashcode相同,那么两个对象比较的euqls方法为真,反之亦然

    所以在Object的equals方法的注释里有这么一段话:

    * Note that it is generally necessary to override the {@code hashCode}

    * method whenever this method is overridden, so as to maintain the

    * general contract for the {@code hashCode} method, which states

    * that equal objects must have equal hash codes.

    大概意思也就是:为了维护 hashcode 方法的一般合约,即:相同的对象必须要具备相同的哈希值在重写了equals 方法后   有必要   重写 hashcode方法注意这里是有必要,没有一定 强制重写。

    重写了equals却没有重写hashcode,一般不会有什么问题,但要是要用到hashcode作为比较的条件的时候才会有问题,比方说对象作为map的Key等。

    hashCode方法的主要作用是为了配合基于散列的集合一起正常运行,这样的散列集合包括HashSet、HashMap以及HashTable。


    扯远了,言归正传。上面讲到了类加载器的原理与作用,下面来分析一下类加载器的类型。

    4.类加载器的分类

    从java虚拟机的角度来讲,只分为两种类加载器,一种叫做启动类加载器,这个加载器有C++实现,是虚拟机的一部分。另外一种就是其他的类加载器,这些类都是有java语言实现,独立于虚拟机之外,并且都有一个共同点:继承自抽象类java.lang.ClassLoader

    从java开发人员的角度来看,分的更加细致,绝大部分的java程序都会提供以下三种系统类加载器。

    1)启动类加载器(Bootstrap ClassLoader 引导类加载器),这个类负责将放在<JAVA_HOME>\lib目录下,并且是虚拟机识别的(仅仅按照文件名识别,名字不符合的类库即使在lib目录下也不会被夹在)类库加载到虚拟机内存中。启动类加载器无法被java程序直接引用,如果要把加载请求委派给启动类加载器,那就直接用null代替即可。

    2)扩展类加载器(Extension ClassLoader)负责加载<JAVA_HOME>\lib\ext目录下的类库

    3)应用程序类加载器(Application ClassLoader)也称为系统类加载器,负责加载用户类路径上所指定的类库,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。通俗的来讲:一般情况下,我们自己写的类是由这个加载器加载。

    双亲委派模型  如下图所示:

    双亲委派模型

    这个模型执行的总体意思也就是:如果一个类收到了类加载请求,它首先自己不会去加载这个类,而是将这个请求委派给父类的加载器去完成,父类的加载器也不会首先去加载,他会将这个请求委派给自己的父类去完成,如此往复直到顶层的 启动类加载器。只有当父类加载器反馈说自己无法完成这个加载请求时(在它的搜索范围类没有找到所需的类),父类会一层一层往下通知,子类加载器才会自己去加载这个类。

    通俗来讲就是这样:

    儿子需要一个玩具月亮,就跟老爹讲,老爹懒得理他,就跟自己的老爹说,你孙子要个月亮,爷爷一听孙子要个月亮,就在自己力所能及的范围内搜索,结果发现自己找不到,然后爷爷就对爸爸说,儿子,我找不到孙子要的月亮,还是你自己来找吧,于是爸爸也在自己的搜索范围类搜索,发现自己也找不到这个月亮,于是也对儿子说,儿子,你老子找不到这个月亮,这个事儿呀还是得你自己来,儿子一听你们都找不到那还是我自己来吧,自己找找找呀找到了,拿着月亮玩去了,要是找遍了没找到,儿子就躲一边哭(报找不到类的异常)

    这个称之为双亲委派模型,这个模型的作用是java类随着他得类加载器一起具备了一种带有优先层次的关系。例如类java.lang.Object,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给顶端的启动类加载器去加载,因此Object类在程序的各种类加载环境中都是同一个类。相反,如果不是用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个java.lang.Object的类,并放在classPath中,那么系统中将出现多个不同的Object类,java体系中最基础的行为也无法保证。

    双亲委派模型对于保证java程序的稳定运行很重要,但实现对相当简单逻辑很清楚:先检查类是否被加载过,若没有就调用父加载器的loadClass方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载器加载失败,抛出异常,再调用自己的findClass方法进行加载。

    双亲委派模型中的父子关系不是由继承关系实现的,而是以组合关系来复用父加载器的代码

    双亲委派模型被广泛应用于之后几乎所有的java程序,但却并不是一个强制的模型,而是设计者推荐给开发者的一种类加载实现方式

    相关文章

      网友评论

        本文标题:类加载器工作原理

        本文链接:https://www.haomeiwen.com/subject/nxymhftx.html