文章目录
JVM类加载机制
一、类加载
1、概述
虚拟机把描述类的数据从Class文件加载到内存,并对加载的数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
Class文件内容解析
与那些在编译时需要进行连接工作的语言不同,Java中类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略虽然会在类加载时增加一些性能开销,但是为Java应用程序提供高度的灵活性(运行期动态加载和动态连接)。
Class文件并非特指某个存在于具体磁盘的文件,Class文件是一串二进制字节流,无论以何种形式存在都可以。
2、JVM加载class文件的原理
JVM中类的装载是由类加载器和Classloade来实现的,ClassLoader是一个重要的Java运行时系统组件,它是负责在运行时查找和装入类文件的类。Java类的加载是动态的,它并不会一次性将所有的类全部加载后在运行,而是保证程序运行的基础类完全加载到JVM中,其他类需要的时候在加载。
Java中所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,几乎不关系类的加载,因为这些都是隐式装载的,除非有特殊的用法,像反射,就需要显式的加载所需要的类。类装载方式,有两种:
- 隐式装载:程序在运行过程中当碰到通过new等方法创建对象时,就会隐式调用类加载器加载对应类到JVM中。
- 显式装载:通过class.forname()等方法,显式加载需要的类。
二、类加载时机
1、类的生命周期

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序进行,而解析阶段则不一定,它在某些情况下可能在初始化阶段后在开始,如果解析一旦在初始化之后开始,这就是我们经常所说的“动态绑定”。这些阶段通常都是互相交叉的混合式进行,各个阶段只保证按部就班的开始,并不保证按部就班的进行或完成。
2、初始化时机(类加载时机)
JVM规范虽然没有强制性约束加载什么时候开始,这可以交给虚拟机的具体实现来自由把握。但是对于类的初始化,虚拟机规范严格规定了有且只有5种情况必须立即对类进行初始化(加载、验证、准备在此之前开始):
- 遇到new(实例化对象)、getStatic(获取类变量的值)、putStatic(给类变量赋值)、**invokeStatic(调用静态方法)**这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这四条指令最常见的java代码场景是:使用new关键字实例化对象时,读取和设置类的静态变量、静态非字面值常量(静态字面值常量除外)时,调用静态方法时。
- 使用java.lang.reflect包的方法对类进行反射调用时,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类时,如果其父类没有进行初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个主类。
- 当使用JDK 1.7的动态语音支持时。方法句柄所对应的类没有进行过初始化,要先触发其初始化。
如上5种场景又被称为主动引用,除此之外所有引用类的方法都不会触发初始化,称为被动引用,被动引用不会导致类初始化,但不代表类不会经历加载、验证、准备阶段。
被动引用有如下3种常见情况:
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化;
- 通过定义对象数组和集合来引用类,不会触发该类的初始化;
- 类A引用类B的static final常量不会导致类B初始化(注意静态常量必须是字面值常量,否则还是会触发B的初始化)。常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量类的初始化。
对于final类型的静态变量,如果该变量的值在编译器就可以确定下来,那么这个变量相当于“宏变量”,Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。
宏变量:满足下面三个条件的即是宏变量。
- 必须是final修饰的变量;
- 必须在开始时就指定初始值;
- 该初始值必须在编译器就可以确定;
接口加载过程与类加载过程稍有一些不同,接口也有初始化过程与类一致。区别是类初始化时要求其父类全部都已经初始化过了,接口初始化时并不要求其父接口全部都已经完成初始化,只有在真正使用到父接口的时候才会初始化。
三、类加载过程
1、加载
加载就是将表示类的class文件加载进内存(方法区)中以便于虚拟机调用。虚拟机需要完成以下三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
类的加载由类加载器完成,类加载器通常由JVM提供,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。
通过使用不同的类加载器,可以从不同来源获取类的二进制字节流,通常有如下几种来源:
- 从本地Class文件获取,绝大部分程序的类加载方式。
- 从ZIP包、JAR包中读取,这很常见,比如JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM从JAR文件中直接加载该class文件。
- 从网络中获取,这种场景最典型的应用就是Applet。
- 运行时计算生成,这种场景使用最多的就是动态代理技术。把一个Java源文件动态编译,并执行加载。
- 由其他文件生成,典型场景是JSP应用。
- 从数据库读取,这种场景相对少见,例如有些中间件服务器。
类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。
如果被加载的是一个数组类型,数组类型是一个比较特殊的类型,数组类本身不通过类加载器加载,而是由虚拟机直接加载,但是数组类的元素类型却需要加载器加载,加载器会加载完数组的元素类型后将该数组绑定到相应的加载器上,然后与该类加载器一起绑定标识唯一性。
2、连接
类被加载之后,系统会为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中,类连接又可分为3个阶段。
(1)验证
这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
文件格式验证是基于二进制字节流进行的,通过了这个阶段的验证,字节流才会进入内存的方法区中进行存储,后面3个验证阶段元数据验证、字节码验证、符号引用验证是基于方法区的存储结构进行的,不会再直接操作字节流。
主要有四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证。
- 文件格式验证: 主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。
验证点:
(1)是否以魔数0xCAFEBABE开头。
(2)主、次版本号是否在当前虚拟机处理范围之内。
(3)常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
(4)指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
(5)CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。
(6)Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
- 元数据验证:(元数据是指用来描述数据的数据,描述代码间关系,或者代码与其它资源(数据库表)之间内在联系的)对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。目的是对类的元数据信息语义校验,保证不存在不符合Java语言规范的元数据信息。
验证点:
(1)这个类是否有父类(除了java.lang.Object之外,所有类都应当有父类)。
(2)这个类是否继承了不允许被继承的类(被final修饰的类)。
(3)如果这个类不是抽象类,是否实现了其父类或接口之中所要求实现的所有方法。
(4)类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等等)。
- 字节码验证:(字节码流 = 操作码 + 操作数。操作码就是伪指令,操作数就是普通的Java数据,如int,float等等。)验证过程最复杂的阶段,通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。主要针对在元数据验证后,对类的方法体进行验证,保证类方法在运行时不会有危害虚拟机安全的事件出现。
例如:
(1)保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似这样的情况:在操作数栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中。
(2)保证跳转指令不会跳转到方法体以外的字节码指令上。
(3)保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险不合法的。
- 符号引用验证:主要发生在虚拟机将符号引用转化为直接引用的时候进行校验,这个转化动作是发生在解析阶段。符号引用验证可以看做是对类自身以外(常量池的各种符号引用)的信息进行匹配性的校验。目的是确保解析动作能正常执行。
通常需要校验以下内容:
(1)符号引用中通过字符串描述的全限定名是否能够找到对应的类。
(2)在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
(3)符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问。
验证阶段对于虚拟机的类加载机制来说,是一个非常重要、但不一定是必要的阶段,对程序运行期没有影响。如果所运行的全部代码都已经被反复使用和验证过,在实施阶段就可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,从而缩短虚拟机类加载的时间。
(2)准备
正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存将在方法区中分配。这里的类变量初始值通常是指数据类型的零值,真正的初始化赋值是在初始化阶段进行的。
注意:这个时候进行内存分配的仅包括类变量(static修饰),而不包括实例变量,类变量会被分配到方法区,实例变量将会在对象实例化时随对象一起被分配在Java堆中。只对static修饰的静态变量进行内存分配、赋默认值,对final的静态字面值常量直接赋初值(赋初值不是赋默认值,如果不是字面值静态常量,那么会和静态变量一样赋默认值)。
(3)解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用(内存地址)的过程。符号引用可以理解为这个引用的名字(标志符),直接引用代表这个引用的目标指针、相对偏移量或者间接定位到目标的句柄。对于同一个符号引用可能会出现多次解析,虚拟机可能会对第一次解析的结果进行缓存。
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,如包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
- 直接引用:是直接指向目标的指针、相对偏移量或是一个能够间接定位的句柄,如指向方法区某个类的一个指针。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般都不相同,如果有了直接引用,那引用的目标必定已经在内存中存在。
动态连接
大部分JVM的实现都是延迟加载或者动态连接。它的意思就是JVM装载某个类A时,如果类A中有引用其他类B,虚拟机并不会将这个类B也同时装载进JVM内存,而是等到执行的时候才去装载。而这个被引用的B类在引用它的类A中的表现形式主要被登记在了符号表中,而解析的过程就是当需要用到被引用类B的时候,将引用类B在引用类A的符号引用名改为内存里的直接引用。这就是解析发生时间不可预料的原因,而且这个阶段是发生在方法区中的。
解析主要针对:类或接口,类方法,接口方法,字段,方法属性,方法句柄,调用点限定符。
- 类或接口的解析(注意数组类和非数组类)
假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的引用,那虚拟机完成整个解析过程需要以下3个步骤:
(1)如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。
(2)如果C是一个数组类型,并且数组的元素类型为对象,那将会按照第1点的规则加载数组元素类型。
(3)如果上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为了一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,确认D是否具有对C的访问权限。如果发现不具备访问权限,则抛出java.lang.IllegalAccessError异常。
- 字段(简单名称+字段描述符)解析(注意递归搜索)
要解析一个未被解析过的字段符号引用,首先解析字段表内class_index项中索引的CONSTANT_Class_info符号引用,也就是字段所属的类或接口的符号引用,如果解析完成,将这个字段所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续字段的搜索。
(1)如果C 本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
(2)否则,如果C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
(3)否则,如果C 不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
(4)否则,查找失败,抛出java.lang.NoSuchFieldError异常。
如果查找过程成功返回了引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常。
如果有一个同名字段同时出现在C的接口和父类中,或者同时在自己的父类或多个接口中出现,那编译器可能拒绝编译,并提示”The field xxx is ambiguous”。
- 类方法解析(注意递归搜索)
首先解析类方法表内class_index项中索引的CONSTANT_Class_info符号引用,也就是方法所属的类或接口的符号引用,如果解析完成,将这个类方法所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续类方法的搜索。
(1)类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C 是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。
(2)如果通过了第一步,在类C 中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
(3)否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
(4)否则,在类C实现的接口列表以及他们的父接口中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在相匹配的方法,说明类C是一个抽象类这时查找结束,抛出java.lang.AbstractMethodError异常。
(5)否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError。
最后,如果查找成功返回了直接引用,将会对这个方法进行权限验证,如果发现不具备此方法的访问权限,则抛出java.lang.IllegalAccessError异常。
- 接口方法解析(注意递归搜索)
首先解析接口方法表内class_index项中索引的CONSTANT_Class_info符号引用,也就是方法所属的类或接口的符号引用,如果解析完成,将这个接口方法所属的接口用C表示,虚拟机规范要求按照如下步骤对C进行后续接口方法的搜索。
(1)与类解析方法不同,如果在接口方法表中发现class_index中的索引C是个类而不是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。
(2)否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
(3)否则,在接口C的父接口中递归查找,直到java.lang.Object类(查找范围包括Object类)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
(4)否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError。
由于接口中所有的方法默认都是public的,所以不存在访问权限的问题,因此接口方法的符号解析应当不会抛出java.lang.IllegalAccessError异常。
3、初始化
初始化是对类变量和静态代码块进行赋值和执行,这个时候虚拟机会为有类变量和静态代码块的类或者接口生成clinit()方法(不是构造方法)。类初始化阶段是类加载过程的最后一步,到了这个阶段才真正开始执行类中定义的Java程序代码(或者说是字节码)。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。
虚拟机规范定义了5种情况,会触发类的初始化阶段:
- new一个对象、读取一个类静态字段、调用一个类的静态方法的时候
- 对类进行反射调用的时候
- 初始化一个类,发现父类还没有初始化,则先初始化父类
- main方法开始执行时所在的类
- 当使用JDK 1.7的动态语音支持时
有三种引用类的方式不会触发初始化(也就是类的加载):
- 通过子类引用父类的静态字段,不会导致子类初始化
- 通过数组定义来引用类,不会触发此类的初始化
- 引用另一个类中的常量不会触发另一个类的初始化,原因在于“常量传播优化” ,常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量类的初始化。
需要注意的问题:
- 编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,而定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问;
- 初始化方法执行的顺序,虚拟机会保证在子类的初始化方法执行之前,父类的初始化方法已经执行完毕,因此在虚拟机中第一个被执行的类初始化方法一定是java.lang.Object。也意味着父类中定义的静态语句块要优先于子类的变量赋值操作;
- clinit ()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成clinit()方法。这个初始化方法是被jvm隐式调用的,它们绝对不会直接被用任何jvm指令调用,仅作为类初始化进程的一部分被间接的调用。
- 接口中不能使用静态语句块,但仍然有变量初始化的操作,因此接口与类一样都会生成clinit()方法,但与类不同的是,执行接口的初始化方法之前,不需要先执行父接口的初始化方法。只有当父接口中定义的变量使用时,才会执行父接口的初始化方法。另外,接口的实现类在初始化时也一样不会执行接口的clinit()方法。
- 虚拟机会保证一个类的clinit()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的clinit()方法,其他线程都需要阻塞等待,当活动线程执行类初始化方法完毕后,释放锁,但是其他线程也不会在执行方法了。(阻塞的线程唤醒之后不会再次进入这个类的clinit()方法,因为同一类加载器下,一个类只会初始化一次)
四、类加载器
1、类和类加载器
类加载器就是用于实现类加载动作(通过一个类的全限定名来获取定义此类的二进制字节流)的代码模块。类加载器负责加载所有的类,为所有被载入内存的类生成一个java.lang.Class实例对象。一个类被加载到JVM中,同一个类就不会被再次载入了。
正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。
- 在Java中,一个类用其全限定类名(包名和类名)作为标识,
- 在JVM中, 一个类用其全限定类名和其类加载器作为其唯一标识。(类加载器的命名空间)对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类命名空间。
JVM预定义有三种类加载器,当一个JVM启动的时候,Java开始使用如下三种类加载器。
启动类加载器是虚拟机的一部分,所有其他的类加载器独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
启动类加载器(Bootstrap ClassLoader):用来加载Java的核心类,是用原生代码来实现的,并不继承自java.lang.ClassLoader,负责将存放在<JAVA_HOME>\lib目录中的或者指定路径中虚拟机识别的所有的类库加载到虚拟机内存中,启动类加载器无法被Java程序直接引用。包名是java.xxx(如:java.lang.Object)
扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录中的或者由java.ext.dirs系统变量所指定的目录中的所有类库,开发者可以直接使用扩展类加载器。包名是javax.xxx(如:javax.swing.xxx)
应用程序类加载器(Applcation ClassLoader):又称系统类加载器,负责加载用户类路径(ClaassPath)上所指定的类库,这个加载器就是ClassLoader的getSystemClassLoader的返回值,这个也是默认的类加载器。Java应用的类都是由它来完成加载的,可以通过ClassLoader.getSystemClassLoader()来获取它。
- 线程上下文类加载器(ThreadContextClassLoader)(TCCL):用于解决双亲委托模型的缺陷,父类加载器请求子类加载器去完成类加载。
- 用户自定义类加载器(UserDefinedClassLoader):由用户自定义类的加载规则,可以手动控制加载过程中的步骤。
2、全盘负责
当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
3、双亲委派模型
JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委派给父类加载器,依次递归,所有的加载请求最终都应该传送到顶层的启动类加载器中,如果父类加载器可以完成类加载任务,就成功返回;如果父类加载器无法完成此加载任务(不在父类加载的搜索范围内),才会自己去加载。

1.首先应用类加载器会收到加载类的请求,收到请求后不立即加载,而是调用其父类加载器即扩展类加载器去加载。
2.扩展类加载器收到加载请求后也不会立即加载,而是调用其父类加载器启动类加载器去加载。
3.启动类加载器收到加载请求后,会去<JAVA_HOME>/lib路径下寻找需要加载的类,若找到则加载,若找不到则回退,请求其子类加载器即扩展类加载器去加载。
4.扩展类加载器收到请求后,会去<JAVA_HOME>/lib/ext路径下寻找需要加载的类,若找到则加载,若找不到则回退,请求其子类加载器即应用类加载器去加载。
5.应用类加载器收到请求后,会去当前类路径或者导入类库的路径下寻找需要加载的类,若找到则加载,若找不到则抛出ClassNotFoundException异常。
双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在 java.lang.ClassLoader 的 loadClass() 中,相关代码如下:
先检查请求的类是否被加载过了,若没有加载则调用父加载器的loadClass()方法,若父加载器为空,则默认使用启动类加载器作为父加载器。如果父加载器失败,抛出classNotFoundException异常后,调用自己的findClass()方法进行加载。
protected synchronized Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException
{
//首先检查请求的类是否被加载过了
Class c=findLoadedClass(name);
if(c==null){
try{
if(parent!=null){
c=parent.loadClass(name,false);
}else{
c=findBootstrapClassOrNull(name);
}
}catch(ClassNotFoundException e){
//如果父类加载器抛出classNotFoundException
//说明父类加载器无法完成加载请求
}
if(c==null){
//在父类加载器无法加载的时候
//在调用本身的findClass方法来进行类加载
c=findClass(name);
}
}
if(resolve){
resolveClass(c);
}
return c;
}
ClassLoader 中与加载类相关的方法:
4、使用双亲委派加载机制的优点
- 1、Java类随着它的类加载器一起具备了一种优先级的层次关系,通过这种层次关系可以避免类的重复加载,当父类加载器已经加载了该类时,就没必要子类加载器再加载一次。不同的类加载器分别负责所搜索范围内的类加载工作,这样能保证同一个类在使用中才不会出现不相等的类。
JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类。
- 2、防止Java核心API库被随意篡改,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委派模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,就不会重新加载网络传递过来的java.lang.Integer,而直接返回已加载过的Integer.class。
如 何 破 坏 双 亲 委 派 机 制 ? \color{green}{如何破坏双亲委派机制?}如何破坏双亲委派机制?
如果不想打破双亲委派模型,继续沿用双亲委派机制自定义的类加载器,只需继承ClassLoader类并重写findClass()方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。而如果想打破双亲委派模型则只需要重写loadClass()方法,典型的打破双亲委派模型的框架和中间件有tomcat与OSGi。
定义一个继承ClassLoader的类,除了重写findClass()方法外还要重写loadClass()方法,这里loadClass()方法默认是双亲委派机制,要想打破,必须重写loadClass()方法,即这里先尝试交由System类加载器加载,加载失败才会由自己加载。
public class TestClassLoaderN extends ClassLoader {
private String name;
public TestClassLoaderN(ClassLoader parent, String name) {
super(parent);
this.name = name;
}
@Override
public String toString() {
return this.name;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> clazz = null;
ClassLoader system = getSystemClassLoader();
try {
clazz = system.loadClass(name);
} catch (Exception e) {
// ignore
}
if (clazz != null)
return clazz;
clazz = findClass(name);
return clazz;
}
@Override
public Class<?> findClass(String name) {
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
is = new FileInputStream(new File("d:/Test.class"));
int c = 0;
while (-1 != (c = is.read())) {
baos.write(c);
}
data = baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
is.close();
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return this.defineClass(name, data, 0, data.length);
}
public static void main(String[] args) {
TestClassLoaderN loader = new TestClassLoaderN(
TestClassLoaderN.class.getClassLoader(), "TestLoaderN");
Class clazz;
try {
clazz = loader.loadClass("test.classloader.Test");
Object object = clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
}
双亲委派模型的三次破坏
1.第一次破坏
由于双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则在JDK1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法唯一逻辑就是去调用自己的loadClass()。
解决方案:JDK1.2之后已不再提倡用户再去覆盖loadClass()方法,应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里,如果父类加载器加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派模型的。
2.第二次破坏
双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷所导致的,双亲委派很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API.如果基础类又要调用回用户的代码,那该么办?一个典型的例子就是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码,但启动类加载器不可能“认识”这些代码。
解决方案:Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,父类加载器请求子类加载器去完成类加载的过程,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。
3.第三次破坏
双亲委派模型的第三次“被破坏”是由于用户对程序的动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是应用程序像计算机外设一样,机器不用重启,只要部署上就能用。
OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当受到类加载请求时,OSGi将按照下面的顺序进行类搜索:
1)将java.*开头的类委派给父类加载器加载。
2)否则,将委派列表名单内的类委派给父类加载器加载。
3)否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
7)否则,类加载器失败。
4、缓存机制
缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换Class对象,存入缓存区中。
5、Class对象、ClassLoad、字节码、class文件之间的联系
Class对象并没有规定实在Java堆中,Class对象、ClassLoad、字节码、class文件之间的联系如下图: