在Java内存区域介绍, 及垃圾收集中都有提到过, 方法区这个概念, 存储的是Java的类信息, 当Java类被加载以后, 就会被存储到方法区中。html
那么Java类是如何被加载的呢?Jvm又是如何解读 class 文件, 全限定名等等相关的东西又是怎样融入Java的体系中呢?java
这里的class文件并不是仅仅是指 .class文件, 而是符合Java虚拟机规范的 class文件或相应的字节流。甚至于 class文件并不是与Java强相关, 只要可以被相应的编译器解读并生成相应的 class文件,在这里并不在意它的语言来源到底是什么。windows
并不是仅仅是class文件有这种特性,对于计算机而言,自己也是如此,只要可以转换为相应的寄存器指令,并不在意,也不须要管指令的生成来源是什么,而Jvm虚拟机正是解读字节码指令的平台。数组
class文件是一组以8位字节为基础单位的二进制流,因此class文件的数据存储是按照严格的规则进行存储,按照顺序解读,每一个字节,每一个位置表明什么都是被严格限定的,它没有相关的描述符, 只有这样才可以被jvm解读。安全
class文件中只有两种数据结构:网络
无符号数数据结构
无符号数属于基本数据类型,以u1,u2,u4,u8分别来表明:1,2,4,8个字节的无符号数。多线程
无符号数能够用来描述:数字,索引引用,数量值或按照UTF-8编码构成的字符串值。框架
换句话说,就是:数值,能够被用来作相关运算的数值;索引;表示表大小,属性长度等等的数量值;以及存储的字面量。 包括 变量名, 字符串常量等其余值。jvm
表
表, 是有层次关系的复合数据结构。
class文件的解读真的是没有什么奥秘可言,均是人为规定了 在某个特定的位置表示什么意义,该如何被解读, 当拿到一个class文件只须要按照Java虚拟机规范中一点点解读,就可以将整个class文件翻译成可读懂的文件, 甚至于翻译成Java代码也不是问题。
eg. 每一个class文件的头四个字节都是0xCAFFBABA,这个魔数值用来表示当前文件是能够被JVM解读的class文件,也是文件类型的真正描述,毕竟咱们都知道后缀名能够被随意更改而不影响文件使用特定的方式进行解读。
eg. 紧接着的魔数值得四个字节存储的class文件的版本号, 前两个是次版本号,后两个是主版本号。 主版本号时从45开始,而每向上一个主版本则加一。
JVM虚拟机能够解读更低版本的class文件,而对高版本的class文件拒绝解读。
而剩余的class文件解读方式都是诸如此类的方式。
固然, 咱们不须要本身费劲的去将class文件转换为16进制,而后一个个去解读它。(windows中以16进制的方式显示 能够采用 WinHex软件)
在Java环境下,咱们仅仅须要
javap -verbose <classpath>
便可,但仅可以输出public 方法,如果想知道更全面的信息
javap -h
有相关的提示信息,其使用语法则是:
javap <options> <classes>
class文件中的常量池,之因此要提到这个概念,与以后要提到的一个概念:动态链接,息息相关, 所以须要略作介绍。
直接看代码:
public class TestClass{ public int a = 1; public static final String b = "zyzdisciple"; public String myName = "zyzdisciple2"; public static int re(int a, final int b) { return a + b; } }
通过编译后, javap -verbose TestClass.class,截取其常量池片断:
Constant pool: #1 = Methodref #6.#22 // java/lang/Object."<init>":( #2 = Fieldref #5.#23 // TestClass.a:I #3 = String #24 // zyzdisciple2 #4 = Fieldref #5.#25 // TestClass.myName:Ljava/lang #5 = Class #26 // TestClass #6 = Class #27 // java/lang/Object #7 = Utf8 a #8 = Utf8 I #9 = Utf8 b #10 = Utf8 Ljava/lang/String; #11 = Utf8 ConstantValue #12 = String #28 // zyzdisciple #13 = Utf8 myName #14 = Utf8 <init> #15 = Utf8 ()V #16 = Utf8 Code #17 = Utf8 LineNumberTable #18 = Utf8 re #19 = Utf8 (II)I #20 = Utf8 SourceFile #21 = Utf8 TestClass.java #22 = NameAndType #14:#15 // "<init>":()V #23 = NameAndType #7:#8 // a:I #24 = Utf8 zyzdisciple2 #25 = NameAndType #13:#10 // myName:Ljava/lang/String; #26 = Utf8 TestClass #27 = Utf8 java/lang/Object #28 = Utf8 zyzdisciple
在常量池中主要存储两大类型常量: 字面量 和 符号引用:
字面量: 文本字符串, 声明为final的常量值。
如上例子中的 #28 存储的就是常量, 同时也做为字面量 其中的 String, #12 #3 都是字面量。
符号引用:
符号引用包括下面三类常量:
类和接口的全限定名; #5 #6(TestClass继承自Object, 父类的全限定名也存储为符号引用。) 在常量池中存储的类全限定名有如下几种:
目前我试过且能想到的只有这几种。
方法的名称及描述符
字段的名称很少解释, 而这里的描述符则是指用来描述字段的数据类型,方法的参数列表(包括数量,类型,及顺序)及返回值。
其描述符与类型的关系对应以下:
B byte; C char; D double; F float; I int; J long;
S short; Z boolean; V void; L 对象类型 eg. Ljava/langg/Object
正是经过符号引用, 来指向对应的常量,从上述也能够看出来, 对于class而言,方法的返回值类型不一样,其实应该表示的是不一样的方法,由于其描述符不一样。
但在编译器中却不容许这种状况出现,不只不能重载, 也不能进行重写。都会提示错误。
既然Java文件编译以后生成的class文件存储有全部相关信息,那么我写的那么一堆代码呢? 那一堆if else for 循环都到哪里去了?
答案就是存储在code属性中。
经过 javap命令就能够看到相关的数据。
java程序方法体中的的代码通过编译器处理后,最终变为字节码指令存储在code属性内, code_length用来表明字节码长度, 而code属性则用来存储一系列的字节码流。 code_length虽然是一个 u4长度的值,理论上最大能够到达 2^32 - 1的长度, 但事实上,在java虚拟机规范中明确要求不可以超过65535条字节码指令,也即U2的长度, 超过即会致使编译失败。
而这种问题最可能出如今 jsp页面的编写中。
一样,有代码以下:
public class TestA extends TestB { public int add (int a, int b) { return a + b; } } Code: stack=2, locals=3, args_size=3 0: iload_1 1: iload_2 2: iadd 3: ireturn
code属性就是上面那样的一条条字节码指令, 字节码指令正是由u1类型的, 表明着某特种特定操做含义的数字(称为操做码)以及跟随其后的零至多个表明此操做所需的参数构成的。
每一个字节码指令都是一个u1类型的单字节, 也就意味着在 java中最多能够表达256种指令。
而java中的大部分指令都是不含操做数的。这点就与基于寄存器的指令集有所不一样。
以前在java内存中提到过,java的操做是基于栈的,最小的操做元素是栈帧,也即一个 slot。
而在上面的代码中也明确看到了: 对全部指令都没有明确的指明操做数是谁,由于所作的操做无非只有两种,出栈,入栈。
而相应的当肯定了指令是谁, 其表明的含义便是对当前栈顶元素进行的操做,栈顶元素也就天然而然的成为了相应的操做数。
而字节码指令自己大都也已经指定了相应的操做数类型到底是什么了。
字节码指令按照功能主要有如下几类:
加载和存储指令
即将数据在栈帧中的局部变量表和操做数栈之间来回传输。
运算指令
用于对两个操做数栈上的值进行某种特定运算,并把结果从新存储在操做数栈顶。
类型转换指令
能够将两种不一样的数值类型进行转换。
对象的建立与访问指令
操做数栈管理指令
控制转移指令
即各类 判断 跳转指令
方法调用和返回指令
异常处理指令
同步指令
即synchronizedx相关。
终于, 咱们在Class文件中存储了种种信息,那么虚拟机又如何,在什么时候,经过怎样的方式将class文件加载,将信息存入内存中呢?
类从被加载到虚拟机内存中开始,到被卸载为止,共经历这样几个阶段:
加载, 验证, 准备, 解析, 初始化, 使用, 卸载7个阶段。
其中前五个阶段是按序开始, 但仅仅是指开始,并无说按序进行,能够交叉进行。
对于什么时候加载一个类,虚拟机中没有明确的规范, 但对什么时候必须初始化一个类, 倒是有着很是明确且严格的约定, “有且只有”如下五种状况:
遇到new、putstatic、getstatic、invokestatic、 这四条字节码指令时,若是类没有进行初始化, 须要先触发其初始化。 这是指, new 一个实例对象时, 读取或设置一个类的静态字段时,以及调用一个类的静态方法时。
使用java.lang.reflect包的方法对类进行反射调用时。
当初始化一个类时, 若是发现其父类尚未进行初始化, 则需触发其父类的初始化。(但接口并不会进行相应的初始化)
当虚拟机启动时,用户须要指定一个要执行的主类(main()入口类),虚拟机会先初始化这个类。
当使用JDK1.5支持时,若是一个java.langl.incoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且这个方法句柄所对应的类没有进行过初始化,则须要先触发其初始化。
而除了上述五种之外, 全部的引用类方法都不会触发初始化, 称为被动引用。
须要注意的是:
MyObject obj = new MyObject[]
这种代码并不会触发MyObject的类的初始化,数组的建立指令是 newarray, 并不在上述的五种状况中。
而接口也一样不会进行初始化, 只有当使用到其中常量时才会进行初始化。
加载是类加载的第一个阶段, 在加载阶段:虚拟机须要完成:
经过一个类的全限定名来获取此类的二进制字节流。
将这个字节流所表明的静态存储结构转化为方法区的运行时内存结构。
在内存中生成一个表明这个类的java.lang.Class对象,并做为方法区中该类的访问入口。
在加载过程 JVM虚拟机给予了最大的灵活性。
能够从ZIP包中读取, 如 jar包, war包
能够从网络中读取
能够运行时计算生成
能够从其余文件生成转换而来 等其余方式。
对于运行时自动生成这点而言,运用的最多的地方是动态代理技术,在java.lang.Proxy 中, 就是用了 ProxyGenerator.generatoerProxyClass 来生成特定的“*$Proxy”的代理类的二进制字节流。
至于什么是动态代理, 参考连接:
文件格式验证
因为第一步加载中的 class的来源多种多样, 所以class文件的安全性就没法保证, 若是从网络上加载了一个非法的 class文件,却没有进行校验,可能会致使虚拟的崩溃,所以, 校验有必定的必要性。
如同大多数校验同样, 当咱们拿到一个.class文件时, 首先校验的是, 这是不是一个class文件,可否正确识别这是一个class文件, 又因为class文件的格式规则有着严格要求, 它是否知足?
凡此种种,其目的只是为了可以将class文件正确解析,并能够按照规则存入方法区, 而在这以后,字节流就被加载进入方法区, 其后的校验均是对方法区的存储结构校验。
元数据验证
在文件格式知足要求且能够正确存入方法区以后,第二步所须要作的事情就是校验是否符合java语言的基础规范自己。
在class文件中存储有相应的字段描述符, class继承关系等等, 但仅仅是存储, 并不会校验是否合理, 如在一个 class中引用了另外一个class的private 字段也是依然能够的。
字节码验证
保证了数据符合java的语法规范以后, 下一步要作的是验证语义是否表达完整, 符合规范。元数据验证更多的是验证是否可进行相应的操做,根据java的关键字, 等基础规则验证是否可行, 而不进行实际上的语义判断。
符号引用验证
这个阶段的校验发生在【解析】阶段,是在将虚拟机的符号引用转换为直接引用的时候,符号引用验证能够看作是对类自身之外的(常量池中的各类符号引用)信息进行匹配性校验,一般需校验:
根据全限定名可否找到对应的类
在指定类中是否存在符合须要的方法,字段
符号引用中的类、字段、方法的访问性是否可以被当前类访问。
准备阶段是为类变量(static)分配内存并设置初始值的阶段。
这里的分配内存并不是是指分配堆内存, 而是将变量的索引存入方法区, 若是是基础变量的话, 也会存入相应的值, 对于:
private static Object obj = new Object();
而言,new Object()所占用的内存依然是在堆中, 不难理解,当再度为static变量赋值为其余对象时, 原有的Object的内存就能够被回收, 所以充分知足 一个对象的相关回收条件。
但若是类字段为 final 的, 在准备阶段还会对类变量进行赋值。
是将虚拟机常量池中的符号引用 替换为 直接引用的过程。
在以前提到过符号引用有三种, 类的接口及全限定名, 字段名称及描述符, 方法名称及描述符。
而直接引用则是直接指向目标的指针、相对偏移量、或是一个可以间接定位到目标的句柄。
虚拟机规范中并未规定解析阶段发生的具体时间,只要求在执行了newarray,checkcast,getfield,getstatic,instanceof,invokedynamic,invokeinterface,invokespecial,invokestatic,invokevritual,ldc,ldc_w, multianewarray,new,putfield和putstatic这16个用于操做符号引用的字节码指令以前,先对其符号引用进行解析。
解析主要针对如下几种:
类或接口、字段、类方法、接口方法、方法类型、方法句柄、点限定符
类或接口的解析
假设当前所处的类为D, 若是要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用, 须要经历如下步骤:
a. 若是C并不是数组类型,那么虚拟机会将表明N的全限定名传递给D的类加载器去加载这个类C,在加载过程当中若是触发了任何异常都会宣告解析失败。
b.若是C是一个数组类型,而且数组的元素类型为对象,也就是N的描述符会是相似”[Ljava.lang.Integer"的形式,那么会按照第a点的规则加载数组元素类型。若是N的描述符如前面所假设的形式,须要加载的元素类型就是“java.lang.Integer”,接着由虚拟机生成一个表明此数组和元素的数组对象。
c.若是上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为一个有效的类或接口了,但在解析完成以前还要进行符号引用验证,确认C是否具有对D的访问权限,若是发现不具体访问权限,将抛出java.lang.IllegalAccessError错误。
字段解析
要解析一个未被解析过的字段符号引用,首先将会对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析,也就是字段所属性的类或接口的符号引用。若是在解析这个类或接口符号引用过程当中出现了任何异常,都会致使字段符号引用解析失败。若是解析成功完成,那么这个字段所属性的类或接口用C表示,虚拟机规范要求以下步骤对C进行后续字段的搜索:
a.若是C自己就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
b.不然,若是C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,若是接口中包含了简单名称答字段描述符都与目标相匹配的字段,则返回该字段的直接引用,查找结束。
c.不然,若是C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,若是在父类中包含了简单名称和字段描述符都与目标匹配的字段,则返回这个字段的直接引用,查找结束。
d.不然,查找失败,招抛出java.lang.NoSuchFieldError错误。
须要注意的是, 虽然在解释中 b.c是拥有前后顺序的, 可是在实际应用中, 若是一个类 其自身实现了接口(非父类实现接口), 同时在继承树,或接口列表中存在相同字段,则是不被容许的。不管接口或是父类都是其直接上级,没法肯定字段的究竟归属是谁。
public class TestB extends TestD implements TestInterface { //public TestC testC = new TestC("objectB"); } public class TestD { public TestC testC = new TestC("objectD"); } public interface TestInterface { TestC testC = new TestC("interface"); } public static void main(String[] args) { TestB testB = new TestB(); //编译报错 TestC testC = testB.testC; System.out.println("~~~~~~~~~~~~~~~"); System.out.println(testC.name); }
此时由于TestB中不存在 testC属性,所以须要向上级查找,没法获知到底是谁, 惟有当将注释放开, 便可经过。
类方法解析
类方法解析的第一个步骤与字段解析同样,也是须要先解析出方法表的class_index项中索引的方法所属性类或接口的符号引用,若是解析成功,依然用C表示这个类,接下来虚拟机将会按照以下步骤进行后续的类方法搜索:
a.类方法和接口方法符号引用的常量类型定义是分开的,若是在类方法表中发现了class_index中索引的C是个接口,那么直接就抛出java.lang.IncompatibleClassChangeError错误。
b.若是通了第a步,在类C中查找是否有简单名称和描述符与目标相匹配的方法,若是有则返回这个方法的直接引用,查找结束。
c.不然,在类C的父类中递归查找是否有简单名称和字段描述符都与目标匹配的方法,则返回这个方法的直接引用,查找结束。
d.不然,在类C实现的接口列表及它们的父接口中递归查找否有简单名称和字段描述符都与目标匹配的方法,说明类C是一个抽象类,这时候查找结束,抛出java.lang.AbstractMethodError错误。
e.不然,宣告查找失败,抛出java.lang.NoSuchMethodError错误。
最后,若是查找过程成功返回了直接引用,将会对晕个方法进行权限验证;若是发现不具务对此方法的访问权限,将抛出java.lang.IllegalAccessError错误。
在这里困惑了很多时间, 这里须要注意的一个地方是: 类方法,仅仅是指 static 方法,并不包括相应的实例方法,与前面提到的字段解析有所不一样,字段解析并无对 是否为 静态进行区分。
这点与java的动态语言调用有关, 下一篇再提。
类的初始化是类加载过程的最后一步,前面的类加载动做,除了在加载阶段用户应用程序能够经过自定义类加载器参与以外,其他动做彻底由虚拟机主导和控制。到了初始化阶段,才真正执行类中定义的Java程序代码(或者说是字节码)。
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段是执行类构造器<clinit>()方法的过程。
<clinit>()方法是由编译器自动收集类中全部类变量的赋值动做和静态语句块(static{})中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块以前的变量,定义在它以后变量,在前面的静态语句块中能够赋值,可是不能访问。
<clinit>()方法与类的构造器<init>()不一样,它不须要显示地调用父类类构造器,虚拟机会保证在子类的<clinit>()方法执行以前,父类的<clinit>()方法已经执行完毕。所以在虚拟机中第一个被执行<clinit>()方法的类确定是java.lang.Object。
<clinit>()方法对于类或接口来讲并非必须的,若是一个类中没有静态语句块,也没有对类变量的赋值操做,那么编译器能够不为这类生成<clinit>()方法。
接口中不能使用静态语句块,但仍然能够有变量初始化的同仁操做,所以接口与类同样都会生成<clinit>()方法,但接口与类不一样的是,执行接口的<clinit>()方法不须要先执行父接口的<clinit>()方法。只有当父接口中定义的变量被使用时,父接口才会初始化。
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁同步,若是多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其它线程都须要阻塞等待,直到活动线程执行<clinit>()方法完毕。若是在一个类的<clinit>()方法中有很耗时的操做,那就可能形成多个线程阻塞,在实际应用中这种阻塞每每是很隐蔽的。
在类加载阶段, “经过一个类的全限定名来获取描述此类的二进制字节流”, 这个动做放在java虚拟机外部去实现, 而实现这个动做的代码模块被称为“类加载器”。
对于任意一个类, 都须要由加载它的类加载器和这个类自己一同确立其在Java虚拟机中的惟一性. 比较两个类(并不是是指实例对象)是否相等,只有在同一种类加载器下才有意义.
相等,指的是, 类的Class对象的equals方法, isInstance()方法的返回值, 还有 instance of 关键字的断定结果.
而你们都有所耳闻的是, Java的类加载机制采起的是双亲委派模型:
从Java虚拟机的角度来说, 只存在两种不一样的类加载器:一是启动类加载器(Bootstrap ClassLoader), 这个类加载器采用 C++语言实现, 是虚拟机自身的一部分,另外一种则是其余全部的类加载器. 这些类都继承自抽象类java.lang.ClassLoader.
从Java开发者的角度来看, 能够分为三种类加载器.
a. 启动类加载器(BootstrapClassLoader) Bootstrap类加载器负责加载rt.jar中的JDK类文件,它是全部类加载器的父加载器.
b. 扩展类加载器(Extension ClassLoader),加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还能够加载-D java.ext.dirs选项指定的目录.
c. 应用程序类加载器(Application ClassLoader), 也即 ClassLoader.getSystemClassLoader()方法的返回值.通常也称做系统类加载器,它负责加载用户类路径上所指定的类库, 若是应用程序中不曾自定义过类加载器, 那么通常这个就是程序中默认的类加载器.
一样的, 执行相同的代码, 我发现与网上获得的结论都是不一样的, 我在这里用的是jdk11的最后一个版本.
public static void main(String[] args) { System.out.println(Thread.currentThread().getContextClassLoader()); Class<?> cls = Main.class; // 取得Class类对象的类加载器信息 System.out.println(cls.getClassLoader()); // 取得Class类对象的类加载器父加载器信息 System.out.println(cls.getClassLoader().getParent()); // 取得Class类对象的类加载器父加载器的父加载器信息 System.out.println(cls.getClassLoader().getParent().getParent()); }
输出的是:
jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc jdk.internal.loader.ClassLoaders$PlatformClassLoader@1e643faf null
是的, 因此在Java11之后的关系应该是, AppClassLoader -> PlatFormClassLoader -> BootStapClassLoader
loadClass
参考:
深刻理解ClassLoader工做机制(jdk1.8)
下面仍是以 jdk1.8为基准
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 { //a: if (parent != null) { c = parent.loadClass(name, false); } else { 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(); //b: c = findClass(name); // this is the defining class loader; record the stats PerfCounter.getParentDelegationTime().addTime(t1 - t0); PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); PerfCounter.getFindClasses().increment(); } } //c: if (resolve) { resolveClass(c); } return c; } }
源码很简单, 查找class是否已经被加载, 若是没有加载:
a: 对应注释中的a处, 调用parent的classLoader, 若是代码都遵循双亲委托模型, 则会一层层向上调用, 直到BootstrapClassLoader.
而parent并不是是继承关系, 是经过组合的形式关联起来.
private final ClassLoader parent;
只有当父类没法加载当前类调用子类的findClass()方法去加载当前类, 层层向下.
b: 注释b处
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
在ClassLoader类中findClass源码如上, 不难发现, 依循双亲委托模型, 只须要实现本身的findClass方法便可. 这才是咱们在本身的实现类中, 惟一要作的事情.
而当咱们拿到对应的Class文件, 或字节流形式的文件, 究竟该以怎样的方式实现本身的findClass, 进而加载这个类呢?
I: Class<?> defineClass(String name, java.nio.ByteBuffer b,ProtectionDomain protectionDomain)
指定保护域(protectionDomain),把ByteBuffer的内容转换成 Java 类。
II: Class<?> defineClass(String name, byte[] b, int off, int len)
把字节数组 b中的内容转换成 Java 类
正是经过这个方法, 咱们能够把本地文件或网络流传入, 进而生成相应的class, 加载进jvm虚拟机.
而另外一个方法也就是注释c处:
protected final void resolveClass(Class<?> c) { //native方法 resolveClass0(c); }
当类被加载进来以后是否须要进行相关的连接操做也正是由这个参数所指定.
连接: 也就是 验证, 准备, 解析相关操做.
至于具体的重写 findCLass() , 案例中就有或者网上百度一大堆, 再也不多说.
双亲委派模型
提到过了, 双亲委派模型是层层向上, 直到最顶层, 若是顶层没法加载类, 再层层向下, 直到抛出ClassNotFoundException; 其中父级与子类之间并不用继承关系进行捆绑, 而是用组合的形式进行绑定.
那么为何要使用双亲委派模型?
若是咱们本身定义了一个 java.lang.String 放在classpath下, 那么当使用时, 系统中就会出现多个 String类, String的种种行为就没法保证, 而若是有人上传的框架或插件中, 偷偷的放入了本身的 java.lang.String, 并用来作一些坏事, 使用插件的人又该怎么预防呢?
所以才须要从上到下一级级进行加载, 若是当前类已经被加载过了, 那么就再也不会被加载. 这就保证了即便定义了本身的 java.lang.String 也会被BootstrapClassLoader加载到正确的 String类, 本身定义的永远不会执行.
破坏双亲委托模型
前面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的所有问题。Java 提供了不少服务提供者接口(Service Provider Interface,SPI),容许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供.
基础类之因此被称为"基础",是由于它们老是做为被调用代码调用的API。
可是,若是基础类又要调用用户的代码,那该怎么办呢。这并不是是不可能的事情,一个典型的例子即是JNDI服务, 它的代码由启动类加载器去加载(在JDK1.3时放进rt.jar)
但JNDI的目的就是对资源进行集中管理和查找,它须要调用独立厂商实现部部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能"认识"之些代码,该怎么办?
线程上下文类加载器正好解决了这个问题。若是不作任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就能够成功的加载到 SPI 实现的类。线程上下文类加载器在不少 SPI 的实现中都会用到。
Thread.currentThread().getContextClassLoader();
至于想了解更多:
对我目前而言, 还未曾涉及到须要用到类加载相关的东西, 不进行相关框架设计, 谈论更多也只是纸上谈兵, 算不得数, 也与当前目的违背.