2.1小节了解了Class文件存储格式的具体细节,在Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能被运行和使用。而虚拟机如何加载这些Class文件?Class文件中的信息进入到虚拟机后会发生什么变化?
:::info 类加载机制:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。 :::
与那些在编译时需要进行连接工作的语言不同,在Java语言里面,类型的加载和连接过程都是在程序运行期间完成的,这样会在类加载时稍微增加一些性能开销,但是却能为Java应用程序提供高度的灵活性,Java中天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。例如,如果编写一个使用接口的应用程序,可以等到运行时再指定其实际的实现。这种组装应用程序的方式广泛应用于Java程序之中。
一、类加载的时机
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段。其中验证、准备和解析三个部分统称为连接(Linking),这七个阶段的发生顺序如图
- 加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始
- 而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)
1.1 类加载过程的第一个阶段:加载
什么情况下需要开始类加载过程的第一个阶段:加载。虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。
:::tips
接口的加载过程与类加载过程稍有一些不同,针对接口需要做一些特殊说明:接口也有初始化过程,这点与类是一致的,上面的代码都是用静态语句块“static{}”来输出初始化信息的,而接口中不能使用“static{}”语句块,但编译器仍然会为接口生成“<clinit>()”类构造器,用于初始化接口中所定义的成员变量。接口与类真正有所区别的是前面讲述的四种“有且仅有”需要开始初始化场景中的第三种:当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到**父接口的时候(如引用接口中定义的常量)才会初始化。**
:::
1.2 初始化条件
虚拟机规范则是严格规定了有且只有四种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
1)遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外,string类型)的时候,以及调用一个类的静态方法的时候。
2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
:::tips 对于这四种会触发类进行初始化的场景,虚拟机规范中使用了一个很强烈的限定语:“有且只有”,这四种场景中的行为称为对一个类进行主动引用。除此之外所有引用类的方式,都不会触发初始化,称为被动引用。下面举三个例子来说明被动引用 :::
被动引用例子
**
1、子类调用父类的静态变量,子类不会被初始化。只有父类被初始化。。对于静态字段,只有直接定义这个字段的类才会被初始化.
2、通过数组定义来引用类,不会触发类的初始化
3、 访问类的常量,不会初始化类
class SuperClass {
static {
System.out.println("superclass init");
}
public static int value = 123;
}
class SubClass extends SuperClass {
static {
System.out.println("subclass init");
}
}
public class Test {
public static void main(String[] args) {
System.out.println(SubClass.value);// 被动应用1
SubClass[] sca = new SubClass[10];// 被动引用2
}
}
:::success 上述代码运行之后,只会输出“SuperClass init!”,而不会输出“SubClass init!”。对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。至于是否要触发子类的加载和验证,在虚拟机规范中并未明确规定,这点取决于虚拟机的具体实现。对于Sun HotSpot虚拟机来说,可通过-XX:+TraceClassLoading参数看到此操作会导致子类的加载。 :::
class ConstClass {
static {
System.out.println("ConstClass init");
}
public static final String HELLOWORLD = "hello world";
}
public class Test {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);// 调用类常量
}
}
上述代码运行之后,也没有输出“ConstClass init!”,这是因为虽然在Java源码中引用了ConstClass类中的常量HELLOWORLD,但是在编译阶段将此常量的值“hello world”存储到了Test**类的常量池中**,对常量ConstClass.HELLOWORLD的引用实际都被转化为Test类对自身常量池的引用了。也就是说实际上Test的Class文件之中并没有ConstClass类的符号引用入口,这两个类在编译成Class之后就不存在任何联系了。
二、类加载的过程
**
1)通过一个类的全限定名来获取定义此类的二进制字节流。
虚拟机规范的这三点要求实际上并不具体,因此虚拟机实现与具体应用的灵活度相当大。例如“通过一个类的全限定名来获取定义此类的二进制字节流”,并没有指明二进制字节流要从一个Class文件中获取,准确地说是根本没有指明要从哪里获取及怎样获取。虚拟机设计团队在加载阶段搭建了一个相当开放的、广阔的舞台,Java发展历程中,充满创造力的开发人员们则在这个舞台上玩出了各种花样,许多举足轻重的Java技术都建立在这一基础之上,例如:从ZIP包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础。
相对于类加载过程的其他阶段,加载阶段(准确地说,是加载阶段中获取类的二进制字节流的动作)是开发期可控性最强的阶段,因为加载阶段既可以使用系统提供的类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员们可以通过定义自己的类加载器去控制
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在**方法区**之中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范未规定此区域的具体数据结构。
3)在Java堆中生成一个代表这个类的java.lang.Class对象(类的字节码对象),作为方法区这些数据的访问入口。
然后在Java堆中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区中的这些类型数据的外部接口。
三、验证
验证的目的是为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。不同的虚拟机对类验证的实现可能会有所不同,但大致都会完成以下四个阶段的验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证。
1)文件格式的验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构进行的。
2)元数据验证:对类的元数据信息进行语义校验(其实就是对类中的各数据类型进行语法校验),保证不存在不符合Java语法规范的元数据信息。
3)字节码验证:该阶段验证的主要工作是进行数据流和控制流分析,对类的方法体进行校验分析,以保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。
4)符号引用验证:这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段中发生该转化,后面会有讲解),主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。
四、准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。 注:
1)这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
2)这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
public static int value=123;
那么变量value在准备阶段过后的初始值为0而不是123,因为这时候尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为123的动作将在初始化阶段才会被执行。
五、解析**
**
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经在内存中。在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。
直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般都不相同,如果有了直接引用,那引用的目标必定已经在内存中存在。
解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info及CONSTANT_InterfaceMethodref_info四种常量类型。下面将讲解这四种引用的解析过程。
1)、类或接口的解析:判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。
2)、字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束。
3)、类方法解析:对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步骤,而且对类方法的匹配搜索,是先搜索父类,再搜索接口。
4)、接口方法解析:与类方法解析步骤类似,只是接口不会有父类,因此,只递归向上搜索父接口就行了。
六、初始化
类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。我们放到后面再讲<clinit>()方法是怎么生成的,在这里,我们先看一下<clinit>()方法执行过程中可能会影响程序运行行为的一些特点和细节,这部分相对更贴近于普通的程序开发人员:
<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序一定是先变量赋值,再静态语句块(无论两者在源文件中出现的顺序如何),因此在静态语句块中可以访问到类变量的初始值了。
<clinit>()方法与类的构造函数(或者说实例构造器<init>()方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。
在Java中对类变量进行初始值设定有两种方式:
:::tips
①声明类变量时指定初始值
②使用静态代码块为类变量指定初始值
:::
JVM初始化步骤:
1)、假如这个类还没有被加载和连接,则程序先加载并连接该类
2)、假如该类的直接父类还没有被初始化,则先初始化其直接父类
3)、假如类中有初始化语句,则系统依次执行这些初始化语句
初始化阶段时执行类构造器方法()的过程。
1)类构造器方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序所决定。
2)类构造器方法与类的构造函数不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的类构造器方法执行之前,父类的类构造器方法已经执行完毕,因此在虚拟机中第一个执行的类构造器方法的类一定是
java.lang.Object。
3)由于父类的类构造器方法方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
4)类构造器方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块也没有对变量的赋值操作,那么编译器可以不为这个类生成类构造器方法。
5)接口中可能会有变量赋值操作,因此接口也会生成类构造器方法。但是接口与类不同,执行接口的类构造器方法不需要先执行父接口的类构造器方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也不会执行接口的类构造器方法。
6)虚拟机会保证一个类的类构造器方法在多线程环境中被正确地加锁和同步。如果有多个线程去同时初始化一个类,那么只会有一个线程去执行这个类的类构造器方法,其它线程都需要阻塞等待,直到活动线程执行类构造器方法完毕。如果在一个类的类构造器方法中有耗时很长的操作,那么就可能造成多个进程阻塞。
七、类加载器
:::success 作用:而类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例 :::
JVM设计者把类加载阶段中的“通过’类全名’来获取定义此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。类加载器在类层次划分、OSGi、热部署、代码加密等领域大放异彩。
7.1 类与类加载器
从虚拟机的角度来说,只存在两种不同的类加载器:
:::info
- 一种是启动类加载器(Bootstrap ClassLoader),该类加载器使用C++语言实现,属于虚拟机自身的一部分。
- 另外一种就是所有其它的类加载器,这些类加载器是由Java语言实现,独立于JVM外部,并且全部继承自抽象类java.lang.ClassLoader。 :::
1)启动类加载器(Bootstrap ClassLoader):负责加载JAVA_HOME\lib目录中并且能被虚拟机识别的类库到JVM内存中,如果名称不符合的类库即使放在lib目录中也不会被加载。开发者无法直接获取到启动类加载器的引用。具体可由启动类加载器加载到的路径可通过System.getProperty(“sun.boot.class.path”)查看。
2)扩展类加载器(Extension ClassLoader):该加载器主要是负责加载JAVA_HOME\lib\,该加载器可以被开发者直接使用。扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的,它负责将JAVA_HOME /lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器,具体可由扩展类加载器加载到的路径可通过System.getProperty("java.ext.dirs")
查看
3)应用程序类加载器(Application ClassLoader):该类加载器也称为系统类加载器,系统类加载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
7.2 类加载器双亲委派模型
JVM在加载类时默认采用的是双亲委派机制。
:::tips 通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归 (本质上就是loadClass函数的递归调用),因此所有的加载请求最终都应该传送到顶层的启动类加载器中。如果父类加载器可以完成这个类加载请求,就成功返回;只有当父类加载器无法完成此加载请求时,子加载器才会尝试自己去加载。事实上,大多数情况下,越基础的类由越上层的加载器进行加载,因为这些基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API(当然,也存在基础类回调用户用户代码的情形,即破坏双亲委派模型的情形)。 关于虚拟机默认的双亲委派机制,我们可以从系统类加载器和扩展类加载器为例作简单分析。 :::
其工作的原理如下所示:
java.lang.ClassLoader中的loadClass(String name)方法的代码中分析出虚拟机默认采用的双亲委派机制到底是什么模样:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
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 { // 递归终止条件
// 由于启动类加载器无法被Java程序直接引用,因此默认用 null 替代
// parent == null就意味着由启动类加载器尝试加载该类,
// 即通过调用 native方法 findBootstrapClass0(String name)加载
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器不能完成加载请求时,再调用自身的findClass方法进行类加载,若加载成功,findClass方法返回的是defineClass方法的返回值
// 注意,若自身也加载不了,会产生ClassNotFoundException异常并向上抛出
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
上面图片给人的直观印象是系统类加载器的父类加载器是标准扩展类加载器,标准扩展类加载器的父类加载器是启动类加载器,下面我们就用代码具体测试一下:
public class LoaderTest {
public static void main(String[] args) {
try {
// 可以直接获取到系统类加载器
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent());
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
} catch (Exception e) {
e.printStackTrace();
}
}
}/* Output:
sun.misc.Launcher$AppClassLoader@6d06d69c
sun.misc.Launcher$ExtClassLoader@70dea4e
null
*///:~
:::warning 可以判定系统类加载器(AppClassLoader)的父加载器是标准扩展类加载器,但是我们试图获取标准扩展类加载器的父类加载器时却得到了null。事实上,由于启动类加载器无法被Java程序直接引用,因此JVM默认直接使用 null 代表启动类加载器。 :::
7.3 类加载双亲委派示例
以上已经简要介绍了虚拟机默认使用的启动类加载器、标准扩展类加载器和系统类加载器,并以三者为例结合JDK代码对JVM默认使用的双亲委派类加载机制做了分析。下面我们就来看一个综合的例子,首先在IDE中建立一个简单的java应用工程,然后写一个简单的JavaBean如下:
package classloader.test.bean;
public class TestBean {
public TestBean() { }
}
在现有当前工程中另外建立一个测试类(ClassLoaderTest.java)内容如下:
package classloader.test.bean;
public class ClassLoaderTest {
public static void main(String[] args) {
try {
//查看当前系统类路径中包含的路径条目
System.out.println(System.getProperty("java.class.path"));
//调用加载当前类的类加载器(这里即为系统类加载器)加载TestBean
Class typeLoaded = Class.forName("classloader.test.bean.TestBean");
//查看被加载的TestBean类型是被那个类加载器加载的
System.out.println(typeLoaded.getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出:
:::success
I:\AlgorithmPractice\TestClassLoader\bin
sun.misc.Launcher$AppClassLoader@6150818a
:::
将当前工程输出目录下的TestBean.class打包进test.jar剪贴到
:::success
I:\AlgorithmPractice\TestClassLoader\bin
sun.misc.Launcher$ExtClassLoader@15db9742
:::
对比上面的两个结果,我们明显可以验证前面说的双亲委派机制:系统类加载器在接到加载classloader.test.bean.TestBean类型的请求时,首先将请求委派给父类加载器(标准扩展类加载器),标准扩展类加载器抢先完成了加载请求。
最后,将test.jar拷贝一份到
:::success
I:\AlgorithmPractice\TestClassLoader\bin
sun.misc.Launcher$ExtClassLoader@15db9742
:::
可以看到,后两次输出结果一致。那就是说,放置到
7.4 双亲委派类模型的意义
「双亲委派机制」最大的好处是避免自定义类和核心类库冲突。比如我们大量使用的 java.lang.String
类,如果我们自己写的一个 String 类被加载成功,那对于应用系统来说完全是毁灭性的破坏。我们可以尝试着写一个自定义的 String 类,将其包也设置为 java.lang
:
package java.lang;
public class String {
private int n;
public String(int n) {
this.n = n;
}
public String toLowerCase() {
return new String(this.n + 100);
}
}
我们将其制作成一个 jar 包,命名为 thief-jdk
,然后写一个测试类尝试加载 java.lang.String
并使用接收一个 int 类型参数的构造方法创建实例。
import java.lang.reflect.Constructor;
public class Test {
public static void main(String[] args) throws Exception {
Class<?> clz = Class.forName("java.lang.String");
System.out.println(clz.getClassLoader() == null);
Constructor<?> c = clz.getConstructor(int.class);
String str = (String) c.newInstance(5);
str.toLowerCase();
}
}
运行测试程序:
:::success 程序抛出 NoSuchMethodException 异常 :::
解释:
:::tips
因为 JVM 不能够加载我们自定义的 java.lang.String
,而是从 BootStrapClassLoader 的缓存中返回了核心类库中的 java.lang.String
的实例,且核心类库中的 String 没有接收 int 类型参数的构造方法。同时我们也看到 Class 实例的类加载器是 null
,这也说明了我们拿到的 java.lang.String
的实例确实是由 BootStrapClassLoader 加载的。
:::
7.5 用户自定义类加载器
扩展类加载器和系统类加载器均是继承自 java.lang.ClassLoader抽象类。要创建用户自己的类加载器,只需要扩展java.lang.ClassLoader类,然后覆盖它的findClass(String name)方法即可,该方法根据参数指定类的名字,返回对应的Class对象的引用。我们下面我们就看简要介绍一下抽象类 java.lang.ClassLoader 中几个最重要的方法:
// 加载指定名称(包括包名)的二进制类型,供用户调用的接口
public Class<?> loadClass(String name) throws ClassNotFoundException{ … }
// 加载指定名称(包括包名)的二进制类型,同时指定是否解析(但是这里的resolve参数不一定真正能达到解析的效果),供继承用
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ … }
// findClass方法一般被loadClass方法调用去加载指定名称类,供继承用
protected Class<?> findClass(String name) throws ClassNotFoundException { … }
// 查找名称为 name 的已经被加载过的类,返回的结果是 java.lang.Class 类的实例。
protected final Class<?> findLoadedClass(String name) { ... }
// 定义类型,一般在findClass方法中读取到对应字节码后调用,final的,不能被继承
// 这也从侧面说明:JVM已经实现了对应的具体功能,解析对应的字节码,产生对应的内部数据结构放置到方法区,所以无需覆写,直接调用就可以了)
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{ … }
通过前面的分析,我们可以看出,除了和本地实现密切相关的启动类加载器之外,包括标准扩展类加载器和系统类加载器在内的所有其他类加载器我们都可以当做自定义类加载器来对待,唯一区别是是否被虚拟机默认使用。这里就简要叙述一下一般用户自定义类加载器的工作流程。
:::success
- 继承
java.lang.ClassLoader
类,让 JVM 知道这是一个类加载器 - 重写
findClass(String name)
方法,告诉 JVM 在使用这个类加载器时应该按什么方式去寻找.class
文件 - 调用
defineClass(String name, byte[] b, int off, int len)
方法,让 JVM 加载上一步读取的.class
文件
:::
示例代码:
package demo;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class CustomClassLoader extends ClassLoader {
private String classpath;
public CustomClassLoader(String classpath) {
this.classpath = classpath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String classFilePath = getClassFilePath(name);
byte[] classData = readClassFile(classFilePath);
return defineClass(name, classData, 0, classData.length);
}
public String getClassFilePath(String name) {
if (name.lastIndexOf(".") == -1) {
return classpath + "/" + name + ".class";
} else {
name = name.replace(".", "/");
return classpath + "/" + name + ".class";
}
}
public byte[] readClassFile(String filepath) {
Path path = Paths.get(filepath);
if (!Files.exists(path)) {
return null;
}
try {
return Files.readAllBytes(path);
} catch (IOException e) {
throw new RuntimeException("Can not read class file into byte array");
}
}
public static void main(String[] args) {
CustomClassLoader loader = new CustomClassLoader("D:\\Code\\java\\Leecode");
try {
Class<?> clz = loader.loadClass("demo.Person");
System.out.println(clz.getClassLoader().toString());
Constructor<?> c = clz.getConstructor(String.class);
Object instance = c.newInstance("Leon");
Method method = clz.getDeclaredMethod("say", null);
method.invoke(instance, null);
} catch (Exception e) {
e.printStackTrace();
}
}
}
:::success
示例中我们通过继承 java.lang.ClassLoader
创建了一个自定义类加载器,通过构造方法指定这个类加载器的类路径(classpath)。重写 findClass(String name)
方法自定义类加载的方式,其中 getClassFilePath(String filepath)
方法和 readClassFile(String filepath)
方法用于找到指定的 .class
文件并加载成一个字符数组。最后调用 defineClass(String name, byte[] b, int off, int len)
方法完成类的加载。
:::
在 main()
方法中我们测试加载了一个 Person 类,通过 loadClass(String name)
方法加载一个 Person 类。我们自定义的 findClass(String name)
方法,就是在这里面调用的,我们把这个方法精简展示如下:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 先检查是否已经加载过这个类
Class<?> c = 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) {}
if (c == null) {
// 所有父加载器和根加载器都无法加载
// 使用自定义的 findClass() 方法查找 .class 文件
c = findClass(name);
}
}
return c;
}
}
:::success
可以看到 loadClass(String name)
方法内部是遵循「双亲委派」机制来完成类的加载。在「双亲」都没能成功加载类的情况下才调用我们自定义的 findClass(String name)
方法查找目标类执行加载。
:::
更多可以参考:http://blog.gqylpy.com/gqy/25111/
7.6 常见问题分析
1、由不同的类加载器加载的指定类还是相同的类型吗?
:::tips 在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但在JVM中,一个类用其全名 和 一个ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间。我们可以用两个自定义类加载器去加载某自定义类型(注意不要将自定义类型的字节码放置到系统路径或者扩展路径中,否则会被系统类加载器或扩展类加载器抢先加载),然后用获取到的两个Class实例进行java.lang.Object.equals(…)判断,将会得到不相等的结果,如下所示: :::
public class TestBean {
public static void main(String[] args) throws Exception {
// 一个简单的类加载器,逆向双亲委派机制
// 可以加载与自己在同一路径下的Class文件
ClassLoader myClassLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name)
throws ClassNotFoundException {
try {
String filename = name.substring(name.lastIndexOf(".") + 1)
+ ".class";
InputStream is = getClass().getResourceAsStream(filename);
if (is == null) {
return super.loadClass(name); // 递归调用父类加载器
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (Exception e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myClassLoader.loadClass("classloader.test.bean.TestBean")
.newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof classloader.test.bean.TestBean);
}
}
输出结果:
:::success
class classloader.test.bean.TestBean
false
:::
2、在代码中直接调用Class.forName(String name)方法,到底会触发那个类加载器进行类加载行为?
:::tips Class.forName(String name)默认会使用调用类的类加载器来进行类加载。我们直接来分析一下对应的jdk的代码: :::
@CallerSensitive
public static Class<?> forName(String className) throws ClassNotFoundException {
// 获取到调用者的类
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
// Returns the class's class loader, or null if none.
static ClassLoader getClassLoader(Class<?> caller) {
// 获取调用类(caller)的类型
if (caller == null) {
return null;
}
// 调用java.lang.Class中本地方法获取加载该调用类(caller)的ClassLoader
return caller.getClassLoader0();
}
3、在编写自定义类加载器时,如果没有设定父加载器,那么父加载器是谁?
**
:::tips
在不指定父类加载器的情况下,默认采用系统类加载器。至于为什么是系统类加载器,现在我们来看一下JDK对应的代码实现。众所周知,我们编写自定义的类加载器直接或者间接继承自java.lang.ClassLoader抽象类,对应的无参默认构造函数实现如下:
:::
//摘自java.lang.ClassLoader.java
protected ClassLoader() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
this.parent = getSystemClassLoader();
initialized = true;
}
我们再来看一下对应的getSystemClassLoader()方法的实现:
private static synchronized void initSystemClassLoader() {
//...
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
scl = l.getClassLoader();
//...
}