深入解析Java类生命周期与类加载机制:双亲委派机制详解

更新:11-07 名人轶事 我要投稿 纠错 投诉

大家好,今天给各位分享深入解析Java类生命周期与类加载机制:双亲委派机制详解的一些知识,其中也会对进行解释,文章篇幅可能偏长,如果能碰巧解决你现在面临的问题,别忘了关注本站,现在就马上开始吧!

在Java语言中,类型的加载、连接、初始化过程都是在程序运行过程中完成的。

这种策略在加载类时稍微增加了一些性能开销,但提高了Java 应用程序的灵活性。

Java的可动态扩展的语言特性是依靠运行时动态加载、动态连接的特性来实现的。

2. 类的生命周期

JVM 类的生命周期。类从加载到虚拟机内存到从内存中卸载的生命周期包括7个阶段:

加载、验证、准备、初始化、卸载这五个顺序是固定的,类加载过程必须按照这个顺序一步步开始。

在某些情况下,解析阶段可以在初始化阶段之后开始,以支持Java 语言运行时绑定。

这些阶段通常是交错和混合的,通常在执行另一个阶段时调用和激活一个阶段。

有且只有5种需要类初始化的情况(主动参考):

当遇到new、getstatic、putstatic或invokestatic这四个字节码指令时,最常见的Java代码场景是:使用new关键字实例化对象时,读取或设置类的静态字段(被final修饰的静态字段除外)当调用类的静态方法时,其结果在编译期间已放入常量池中。

使用java.lang.reflect包的方法对类进行反射调用时,如果该类尚未初始化,则需要触发其初始化。

初始化一个类时,如果其父类尚未初始化,则需要先触发其父类的初始化。

虚拟机启动时,用户需要指定一个要执行的主类,虚拟机首先初始化该主类(包含main方法的类)

使用jdk的动态语言支持时,如果一个java.lang.invoke.Methodhandle实例最终解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,且该方法句柄对应的类尚未初始化,则需要首先初始化。

被动参考:

通过子类引用父类的静态字段不会导致子类被初始化。对于静态字段,只有直接定义这个字段的类才会被初始化,所以通过其子类类引用父类中定义的静态字段,只会触发父类的初始化,而不会触发子类的初始化。至于是否触发子类的加载和验证,JVM规范中并没有明确规定。这取决于虚拟机的具体实现。示例代码如下:

包io.ilss.main;

/**

* @作者伊人

* @日期2019-08-20

**/

公共类超类{

静止的{

System.out.println("超类初始化!");

}

公共静态int 值=123;

}

包io.ilss.main;

/**

* @作者伊人

* @日期2019-08-20

**/

公共类子类扩展超类{

静止的{

System.out.println("子类初始化!");

}

}

包io.ilss.main;

/**

* @作者伊人

* @日期2019-08-20

**/

公共类NotInitialization {

公共静态无效主(字符串[] args){

System.out.println(SubClass.value);

}

}通过数组定义引用一个类不会触发该类的初始化。但是触发这个数组元素类对应的数组类的初始化。例如,io.ilss.Demo类对应于[io.ilss.Demo.这不是用户代码的合法类名。它由虚拟机自动生成,直接继承于java.lang.Object 的子类,创建动作由字节码指令newaray 触发。 [io.ilss.Demo类表示io.ilss.Demo对应的一位数组。数组中的所有属性和方法都在此类中实现。 Java的数组访问比C/C++更安全。因为这个类封装了对数组元素的访问(封装在数组访问指令xaload和xastore中)。

包io.ilss.main;

/**

* @作者伊人

* @日期2019-08-20

**/

公共类NotInitialization {

公共静态无效主(字符串[] args){

SuperClass[] superClasses=new SuperClass[10];

}

常量在编译阶段会被存储在调用类的常量池中。本质上,它们并不直接引用定义常量的类,因此不会触发定义常量的类的初始化。这里有一点需要解释一下。如果在此类的第一个位置使用该调用,则将加载类ConstClass。这里说的是在非类中调用这个常量不会初始化ConstClass。这是因为**Java在编译阶段就优化了常量传播,并将hello world的值存储在NotInitialization类的常量池中**。 NotInitialization对“常量HELLO_WORLD”的引用变成了对其自身常量池的引用。事实上**NotInitialization**中不会有任何对ConstClass类的符号引用,两个类编译成Class后也不会有任何联系。

``java

包io.ilss.main;

/**

* @作者伊人

* @日期2019-08-20

**/

公共类ConstClass {

静止的{

System.out.println("ConstClass init");

}

公共静态最终字符串HELLO_WORLD="你好世界";

公共静态无效主(字符串[] args){//1

System.out.println(HELLO_WORLD);

}

}

包io.ilss.main;

导入静态io.ilss.main.ConstClass.HELLO_WORLD;

/**

* @作者伊人

* @日期2019-08-20

**/

公共类NotInitialization {

公共静态无效主(字符串[] args){//2

System.out.println(HELLO_WORLD);

}

}

````

接口和类的加载略有不同。接口也有一个初始化过程。接口中没有static{}代码块,但编译器仍然会为接口生成一个`()`类构造函数,用于初始化接口中定义的成员变量。接口和类的真正区别在于,只有第三种类初始化场景:当一个类被初始化时,它的所有父类都需要被初始化,但是在接口中,它的父类不需要被初始化。所有接口均已初始化,只有在实际使用父接口时才会初始化(如引用接口中定义的常量)

3. 类的加载过程

类加载的全过程:加载、验证、准备、解析和初始化

3.1. 加载

JVM在加载阶段需要完成以下三件事:

获取通过完全限定名称定义类的二进制字节流。

将这个字节流表示的静态存储结构转换为方法区中的运行时数据结构。

在内存中生成一个代表该类的java.lang.Class对象,作为方法区中该类的各种数据的访问入口。

通过一个类的全限定名来获取定义此类的二进制字节流,未指定在哪里或如何获取它。可从:

从zip 包中读取非常常见,并最终成为当前格式:JAR、EAR 和WAR 格式基础知识

从网上获取的,以前有一个Applet(已经过时)做了这个

对于运行时计算和生成,最常见的是动态代理技术。在java.lang.reflect.Proxy中,ProxyGenerator.generateProxyClass用于为特定接口生成"*$Proxy"形式的代理类的二进制字节流。

从其他文件生成:比如JSP应用程序,对应的Class类是由JSP生成的

从数据库获取,这种场景比较少见。

加载阶段获取类的二进制字节流的动作是开发者最可控的。加载阶段可以使用系统提供的引导类加载器来完成,也可以由用户定义的类加载器来控制。如何获得节流。 (即重写类加载器的loadClass() 方法)

对于数组来说,是有区别的。数组类本身不是通过类加载器创建的。它是由Java虚拟机直接创建的。但是,数组类和类加载器仍然紧密相关,因为数组的元素类型是由类加载器创建的。数组类的创建过程遵循以下规则:

如果数组的组件类型(Component Tyep,指从数组中去掉一维的类型)为是引用类型,那么递归地使用本节定义的类加载过程来加载这个组件类型,数组将加载此组件类型。类加载器在类命名空间上标识。

如果数组是组件类型不是引用类型(例如int[]),JVM 会将该数组标记为与引导类加载器关联

数组类的可见性与其组件类型的可见性一致。如果组件类型不是引导类型,则数组类的可见性默认为public。

加载阶段完成后,虚拟机外部的二进制流按照虚拟机要求的格式存储在方法中。方法区中的数据存储格式由虚拟机实现定义。虚拟机规范并没有规定该区域的具体数据结构。然后在内存中实例化一个java.lang.Class类的对象处,该对象将充当程序访问方法区中的这些类型数据的外部接口

对于热点,Class对象比较特殊,它虽然是对象,但是存在了方法区中

加载阶段和连接阶段是交错的。如果加载阶段未完成,则连接阶段可能已经开始,但两个阶段的顺序是固定的。

3.2. 验证

验证阶段一般会完成四个阶段的验证动作:文件格式验证、元数据验证、字节码验证和符号引用验证。

文件格式验证:主要目的是保证输入的字节流能正确地解析并存储于方法区之内格式上符合描述一个Java类型信息的要求。本阶段验证为基于二进制字节流进行。只有通过了这一阶段的验证,字节流才会进入内存的方法区进行存储。接下来的三个验证阶段都是基于方法区的存储结构。是的,字节流将不再被直接操作。

是否以幻数0xCAFEBABE开头

主次版本号是否在当前虚拟机的处理范围内

常量池中的常量中是否存在不支持的常量类型(检查异常标记标志)

指向常量的各种索引值是否指向不存在的常量或者不符合类型的常量?

CONSTANT_Utf8_info类型的常量中是否存在不符合UTF8编码的数据

Class文件各部分以及文件本身是否有删除或附加信息。

……

元数据验证:对字节码描述的信息进行语义分析,确保描述的信息符合Java语言规范的要求;主要目的是对堆类的元数据信息进行语义验证,确保不存在与Java语言规范元数据信息不兼容的情况。

该类是否有父类(除了java.lang.Object之外的所有类都应该有父类)

该类的父类是否继承了不允许继承的类(通过final修饰)

如果这个类不是抽象类,它是否实现了其父类或接口中需要实现的所有方法?

类的字段和方法是否与父类冲突(例如父类的final字段被覆盖,或者存在不符合规则的方法重载,例如方法参数一致,但是返回值类型不同等)

.

字节码验证:通过数据流和控制流分析确定程序语义合法、逻辑。在这个阶段,会对类的方法体进行验证和分析,以确保名为tears的方法在运行时不会做出任何危害虚拟机安全的事情。

确保操作数栈的数据类型和指令代码序列可以随时协同工作。例如,不会出现这样的情况:一个int类型的数据放在操作栈上,但使用时却以long类型的形式加载到局部变量表中。中间,

确保跳转指令不会跳转到方法体之外的字节码指令。

确保方法体中的类型转换有效。例如,将子类传递给父类是安全的,但将父类分配给子类,甚至将对象分配给没有继承关系的无关数据类型,则是危险且非法的。

符号引用验证: 当符号引用转换为直接引用时,会发生此验证,这发生在连接解析的第三阶段。可以将其视为堆类本身以外的信息进行匹配验证。需要验证以下内容:

能否通过拖动符号引用中字符串描述的完全限定名来找到对应的类?

指定类中是否存在与方法以及简单名称描述的方法和字段相匹配的字段描述符。

符号引用中的类、字段、方法的可访问性(private、protected、public、default)是否可以被当前类访问。

.

符号引用的目的是保证解析动作能够正常执行。如果符号引用验证无法通过,

java.lang.IncompatiableClassChangeError 异常的子类将被抛出。

例如:IllegalAccessError、NoSuchFieldError、NoSuchMethodError

3.3. 准备

在正式为类变量分配内存并设置初始值类变量(不是实例变量)的阶段,这些变量使用的内存将分配在方法区处。初始值通常情况零值的类型public static int value=123;

这里的初始值不是值123,而是值int的默认值0。赋值123的putstatic指令需要在类的constructor()方法中,所以要到初始化阶段才会执行赋值。

除了通常的情况特殊情况: 如果类字段的字段属性表中存在ConstantValue 属性,那么在准备阶段该值会被初始化为ConstantValue,如:

公共静态最终int 值=123;注意final对于上面的代码,编译时,javac会根据该值生成ConstantValue属性。在准备阶段,该值将根据ConstantValue 的设置设置为123。

3.4. 解析

解析阶段是JVM将常量池中的符号引用替换为直接引用的过程;符号引用作为CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 和其他类型的常量出现在Class 文件中。

符号引用: 符号引用使用一组符号来描述所描述引用的目标。这些符号可以是任何形式的文字,只要它们可以用来明确地定位目标即可。符号引用与内存布局无关,引用的目标不一定加载到内存中。符号引用的字面形式在JVM 规范的Class 文件格式中有明确定义。

直接参考: 直接参考可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用与JVM内存布局有关。同一个符号引用在不同的JVM中翻译出来的直接引用一般会有所不同。如果存在直接引用,则引用的目标必须已经存在于内存中。

JVM 规范没有指定解析阶段何时发生。它只需要在执行对它们进行操作的字节码之前解析这16 个操作符号引用所使用的符号引用。因此,JVM 可以选择是在类加载器加载类时解析该类,还是在使用符号引用时解析该类。

多次解析相同的符号引用是很常见的。除了invokedynamic之外,还可以缓存第一次解析的结果,避免重复解析。

将直接引用记录在运行时常量池中,并将该常量标记为已解析。

JVM需要确保在同一个实体中,在之前成功解析了符号引用之后,后续的引用解析请求应该始终成功;类似地,如果第一次解析失败,则也应该接受其他指令对该符号的解析请求。同样的例外。

上述规则不适用于invokedynamic。

解析动作主要针对:类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符7类符号引用进行。

类或接口解析假设当前代码所在的类为D,如果要将一个从未解析过的符号引用N解析为对类或接口C的直接引用,那么虚拟机需要以下三个步骤完成整个解析过程。步:

如果C为非数组类型,则虚拟机会将代表N的全限定名传递给D的类加载器来加载这个类C。加载过程中,由于元数据验证、字节码验证的需要,可能会导致其他相关类的加载动作触发,如加载这个类的父类或实现的接口。一旦这个加载过程中出现任何异常,解析过程就会失败。如果C 是数组类型数组的元素类型为对象,即N 的描述符将采用[Ljava/lang/Integer 的形式,即按照第1点的规则加载数组元素类型。如果N的描述符是前面假设的形式,则需要加载的元素类型是“java.lang.Integer”,后面是由虚拟机生成一个代表此数组维度和元素的数组对象。如果上述步骤没有出现异常,那么C实际上已经成为虚拟机中有效的类或接口,但是在解析完成之前还要进行符号引用验证处,确认D是否具备对C的访问权限。如果未找到访问权限,则会抛出java.lang.IllegalAccessError 异常。字段解析解析未解析的字段符号引用。首先解析字段表中class_index项索引的CONSTANT_Class_info符号引用,即字段所属的类或接口的符号引用。如果解析过程中出现任何异常,都会导致字段符号引用解析失败。如果解析成功,则该字段所属的类或接口用C表示。JVM规范要求按照以下步骤在C中搜索后续字段。

如果C 本身包含一个简单名称和字段描述符都与目标匹配的字段,则返回对该字段的直接引用,并且搜索结束。否则,如果位于C中实现了接口,则将为按照继承关系从下往上递归搜索各个接口和它的父接口。如果接口包含简单名称和字段描述符都与目标匹配的字段,则返回对此字段的直接引用,并且搜索结束。否则,如果是C不是java.lang.Object,则以继承关系从下往上递归搜索其父类为基础。如果父类包含简单名称和字段描述符与目标匹配的字段,则将返回对该字段的直接引用,并且搜索将结束。否则,搜索失败并抛出java.lang.NoSuchFieldError 异常。如果是查找过程成功返回了引用,则该字段将为权限验证,如果找到不具备对字段的访问权限,则会抛出java.lang.Ille-galAccessError 异常。

类方法解析首先解析出类方法表的class_index项中的索引的方法所属的类或接口的符号引用。如果解析成功,就用C来代表这个类。那么JVM会按照以下步骤进行后续的类方法搜索。

类方法和接口方法符号引用的常量类型定义是分开的。如果在类方法表中发现class_index中索引的C是一个接口,那么会直接抛出java.lang.IncompleteClassChangeError异常。如果步骤1通过,则查找C类中是否有简单名称和描述符都与目标相匹配的方法,如果有,则返回该方法的直接引用,查找结束。否则,判断C类的父类中递归查找中是否存在简单名称和描述符与目标匹配的方法,如果有,则返回对该方法的直接引用,搜索结束。否则,C类中实现的接口列表及它们的父接口之中递归地搜索是否存在简单名称和描述符与目标匹配的方法。如果有匹配的方法,则说明类C是一个抽象类。这时候搜索就结束了,抛出java。 lang.AbstractMethodError 异常。否则,声明方法搜索失败,并抛出java.lang.NoSuchMethodError。最后,如果搜索过程成功返回直接引用,则此方法将检查权限验证。如果发现该方法没有访问权限,则会抛出java.lang.IllegalAccessError异常。

接口方法解析接口方法还需要首先解析出接口方法表的class_index项中索引的方法所属的类或接口的符号引用。如果解析成功,仍然用C来表示接口。接下来,虚拟机将进行如下操作: 后续接口方法搜索。

与类方法解析不同的是,如果在接口方法表中发现class_index中的索引C是类而不是接口,那么会直接抛出java.lang.Incom-patibleClassChangeError异常。否则,在接口C 中搜索是否有简单名称和描述符与目标匹配的方法。如果是,则返回对此方法的直接引用,然后搜索结束。否则,在接口C 的父接口中递归搜索,直到java.lang.Object 类(搜索范围为包括Object个类),查看是否存在与目标匹配的名称和描述符简单的方法,如果所以, return 对该方法的直接引用,搜索结束。否则,声明方法搜索失败,并抛出java.lang.NoSuchMethodError异常。在接口中,所有方法默认都是public的,所以不存在访问权限问题。因此,接口方法的符号解析不应抛出java.lang.IllegalAccessError。

3.5. 初始化

类初始化阶段是类加载过程的最后一步。除了用户应用程序可以通过自定义类加载器参与加载阶段外,其余动作完全由虚拟机主导和控制。在初始化阶段,类中定义的Java 代码或字节码实际上开始执行。

在准备阶段,变量已被赋予系统所需的初始值。在初始化阶段,类变量和其他资源根据程序员通过程序定制的主观计划进行初始化。或者也可以从另一个角度来表达:初始化阶段是类的constructor()方法执行过程中存在一些可能影响程序运行行为的特征和细节。

() 方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{} 块)中的语句生成的。编译器集合的顺序由语句在源文件中出现的顺序决定。静态语句块中只能访问到定义在静态语句块之前的变量,其中定义之后的变量,在前面的静态语句块可以赋值,但是不能访问公共类Test{

静止的{

我=0;

系统.o

ut.print(i) } static int i = 1; }<clinit>()方法与类的构造函数(或者说实例构造器<clinit>()方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。 由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作static class Parent { public static int a = 1; static { a= 2; } } static class Sub extends Parent { public static int b = a; } public static void main(Strintg[] args) { System.out.println(Sub.b) // 结果为2 }<clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化类,只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞。(需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出<clinit>()方法后,其他线程唤醒之后不会再次进入<clinit>()方法同一个类加载器下,一个类型只会初始化一次) static class DeadLoopClass { static { // 如果不加上这个if语句,编译器将提示“Initializer does not complete normally”并拒绝编译 if (true) { System.out.println(Thread.currentThread() + "init DeadLoopClass"); while (true) { } } } } public static void main(String[] args) { Runnable script = () ->{ System.out.println(Thread.currentThread() + "start"); DeadLoopClass dlc = new DeadLoopClass(); System.out.println(Thread.currentThread() + " run over"); }; Thread thread1 = new Thread(script); Thread thread2 = new Thread(script); thread1.start(); thread2.start(); }Thread[Thread-0,5,main]start Thread[Thread-1,5,main]start Thread[Thread-0,5,main]init DeadLoopClass

4. 类加载器

“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让程序自己决定如何获取所需的类。实现这个动作的代码模块称为“类加载器”。类加载器在类层次划分、OSGi、热部署、代码加密等领域大放异彩,成为了java体系中的一块重要的基石。

4.1 类与类加载器

对于任意一个类,都需要由它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间,比较两个类是否“相等”,是由同一个类加载器加载的前提下才有意义,否则即使两个类是同一个class文件,被同一个虚拟机加载,只要加载他们的类加载器不一样,那这两个类就必定不相等。这里的相等,包括代表类的Class对象的equals、isAssignableFrom、isInstance方法返回的结果。也包括instanceof做的所属关系判定情况。

4.2. 双亲委派模型

从JVM的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言(HotSpot)实现,是虚拟机自身的一部分。另外一种就是其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且都继承自抽象java.lang.ClassLoader。 从Java开发人员的角度来看,绝大部分Java程序都会使用以下3种系统提供的类加载器。 启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在lib目录中,并且是虚拟机识别的(名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户编写自定义类加载器时,需要把加载请求委派给引导类加载器,那就直接使用null代替即可。扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,他负责加载libext目录中的类库,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称他为系统类加载器。他负责加载用户类路径上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义自己的类加载器,一般情况下这个就是程序中的默认类加载器。双亲委派模型(Parents Delegation Model):双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。 双亲委派模型的工作过程是: 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。每一个层次的类加载器都是如此。因此,所有的加载请求最终都应该传送到顶层的启动类加载器中。只有当父加载器反馈自己无法完成这个加载请求时(搜索范围中没有找到所需的类),子加载器才会尝试自己去加载。类加载 - 双亲委派模型使用双亲委派模型来组织类加载器之间的关系,好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。 如类java.lang.Object,它存放在rt.jar之中,无论哪个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写一个称谓java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。 双亲委派的实现代码: ```java protected ClassloadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先,检查类是否已经加载 Classc = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 如果没有从非空父类加载器中找到类, // 则抛出ClassNotFoundException } if (c == null) { // 如果仍然没有找到该类,那么调用findClass来找到该类。 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; } }

4.3. 破坏双亲委派

双亲委托模型并不是一个强制性的约束,而是Java设计者推荐给开发者的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型,但也有例外,双亲委派模型主要出现过3个较大规模的“被破坏”的情况。 由于双亲委派模型在JDK 1.2之后才被引入,为了向前兼容,JDK 1.2之后的java.lang.ClassLoader添加了一个新的protected方法findClass(),在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。 JDK 1.2之后已不提倡用户再去覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里如果父类加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派规则的。 双亲委派很好的解决各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为他们总是作为被用户代码调用的API,但事实往往没有绝对的完美,如果基础类又要调用回用户的代码该怎么解决。 一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,他的代码由启动类加载器去加载(在JDK1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,他需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码, 为了解决这个问题,Java设计团队引入了个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。 有了线程上下文类加载器,就可以做一些“舞弊”的事情了,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这汇总行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。 第三次“被破坏”是由于用户对程序动态性的追求而导致的,“动态性”指的是:代码热替换(HotSwap)、模块热部署(HotDeployment)等,但对于一些生产系统来说,关机重启一次可能就要被列为生产事故,这种情况下热部署就对软件开发者,尤其是企业级软件开发者具有很大的吸引力。 Sun公司所提出的JSR-294、JSR-277规范在与JCP组织的模块化规范之争中落败给JSR-291(即OSGI R4.2),目前OSGi已经称为了业界“事实上”的Java模块话标准,而OSGi实现模块化热部署的关键则是他自定义的类加载器机制的实现。每一个程序模板(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。

好了,本文到此结束,如果可以帮助到大家,还望关注本站哦!

用户评论

来自火星的我

想了解一下java程序是怎么执行的,这篇帖子应该挺有帮助吧!

    有19位网友表示赞同!

妄灸

我一直在学习Java,对类的生命周期和类加载非常感兴趣。

    有19位网友表示赞同!

南宫沐风

这个标题听起来蛮专业的样子,需要细看下!

    有11位网友表示赞同!

月下独酌

双亲委派机制是什么?想了解一下这个概念。

    有9位网友表示赞同!

高冷低能儿

之前没听说过JVM的这种运作机制,感觉很新奇!

    有19位网友表示赞同!

旧事酒浓

学习Java的过程中,这些知识点都是必备的呀!

    有18位网友表示赞同!

你与清晨阳光

分享学习资源是挺有帮助的,感谢作者!

    有17位网友表示赞同!

执拗旧人

这篇文章能让我更深入地理解Java程序的工作原理吗?

    有20位网友表示赞同!

夏至离别

JVM这个东西我一直不太了解,期待这篇详细讲解!

    有12位网友表示赞同!

旧爱剩女

如果能配合图解解释更佳,更容易理解!

    有15位网友表示赞同!

伱德柔情是我的痛。

学习Java的同学应该找来看看这篇帖子吧!

    有13位网友表示赞同!

青山暮雪

类的生命周期和类加载过程是基础知识,还是要搞清楚!

    有15位网友表示赞同!

窒息

感觉双亲委派机制听起来很复杂的样子,希望能讲得通俗易懂!

    有9位网友表示赞同!

刺心爱人i

希望这篇文章能解释得深入浅出,让我能够更好地理解这些概念!

    有15位网友表示赞同!

不离我

想要掌握Java编程更需要了解底层原理,这篇帖子应该很不错!

    有8位网友表示赞同!

自繩自縛

学习Java的过程中,遇到了很多困惑,希望能在这篇文章中找到答案!

    有13位网友表示赞同!

ゞ香草可樂ゞ草莓布丁

期待作者的讲解能让我对JVM有更加清晰的认知!

    有15位网友表示赞同!

别在我面前犯贱

这篇文章是否适用于初学者?

    有11位网友表示赞同!

陌上花

感谢作者分享知识,希望大家都能够从中学习到东西!

    有18位网友表示赞同!

【深入解析Java类生命周期与类加载机制:双亲委派机制详解】相关文章:

1.蛤蟆讨媳妇【哈尼族民间故事】

2.米颠拜石

3.王羲之临池学书

4.清代敢于创新的“浓墨宰相”——刘墉

5.“巧取豪夺”的由来--米芾逸事

6.荒唐洁癖 惜砚如身(米芾逸事)

7.拜石为兄--米芾逸事

8.郑板桥轶事十则

9.王献之被公主抢亲后的悲惨人生

10.史上真实张三丰:在棺材中竟神奇复活

上一篇:手机赚钱应用:每小时100元,真实可靠赚钱软件推荐 下一篇:传奇服务器开设成本及当前优惠价格一览