Java虚拟机-类加载机制与类加载器
Java中类加载、连接和初始化的过程都是在程序运行期间完成的,这些策略虽然会令类加载时增加些性能开销,但是会提高java的灵活性。Java动态扩展的特性就是依赖运行期动态加载和动态连接的特点实现的。
JVM类加载机制
Java源代码被编译为字节码文件后,需要加载进内存才能在程序中被使用。程序启动时并不会一次性加载程序要用的所有class文件,而是根据程序需要,通过Java的类加载机制(ClassLoader)动态加载某个class文件到内存当中。ClassLoader就是用来在运行时加载字节码文件进内存的,加载的过程是线程安全的(如何保证其是线程安全的?)。
Java默认提供三个ClassLoader,分别是BootStrap ClassLoader、Extension ClassLoader和Application ClassLoader,默认使用双亲委派模式进行类加载。双亲委派只是个推荐,也可以不适用此策略,如Tomcat。
启动类加载器负责加载JDK的核心类库,如rt.jar、resources.jar、charsets.jar等,并构造扩展类加载器和系统类加载器,随JVM启动而启动、
扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar、
系统类加载器,负责加载应用程序classpath目录下的所有jar和class文件。
用户可自定义类加载器,通过继承java.lang.ClassLoader类并重写findClass()方法进行
类加载过程中,先后会对字节码文件进行加载、连接(验证、准备、解析)、初始化等步骤,这些步骤都在程序运行期间完成,最终类的描述信息存放在方法区(补充)。。。。。。。其中,解析阶段执行时间不确定,某些情况下可以在初始化阶段后才开始,以支持Java语言运行时绑定机制(动态绑定或晚期绑定)
这就是类加载机制。
随后,被加载的类将被使用、卸载,这就是类的生命周期。
类被加载到虚拟机内存中开始,到卸载为止,整个生命周期包括:加载、连接(验证、准备、解析)、初始化、使用和卸载阶段。
类加载发生时机:
1.虚拟机启动
Java 虚拟机的启动是通过引导类加载器(Bootstrap Class Loader)创建一个初始类(Initial Class)来完成,这个类是由虚拟机的具体实现指定。紧接着,Java 虚拟机链接这个初始类,初始化并调用它的 public void main(String[])方法。之后的整个执行过程都是由对此方法的调用开始。执行 main 方法中的 Java 虚拟机指令可能会导致Java 虚拟机链接另外的一些类或接口,也可能会调用另外的方法。
可能在某种 Java 虚拟机的实现上,初始类会作为命令行参数被提供给虚拟机。当然,虚拟机实现也可以利用一个初始类让类加载器依次加载整个应用。初始类当然也可以选择组合上述的方式来工作。
2.类加载
3.虚拟机退出
Java 虚拟机的退出条件一般是:某些线程调用 Runtime 类或 System 类的 exit 方法,或是 Runtime 类的 halt 方法,并且 Java 安全管理器也允许这些 exit 或 halt 操作。除此之外,在 JNI(Java Native Interface)规范中还描述了当使用 JNI API 来加载和卸载(Load & Unload)Java 虚拟机时,Java 虚拟机的退出过程。
类装载条件:主动使用
类何时触发初始化?主动使用时触发初始化
主动使用:Class只有在要使用到的时候才会被装载,进行初始化。使用是指主动使用.
仅以下情况属于主动使用:
1.遇到new、gestatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,需要触发其初始化。
当用new关键字实例化对象
当调用类的静态方法时。即当使用了字节码invokestatic指令
当用类或接口的静态字段时,或对这些静态字段执行赋值操作时(final常量、已在编译器把结果放入常量池的静态字段除外),如用了getstatic或putstatic指令
2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
注:反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制,这相对好理解为什么需要初始化类。
3.当初始化一个类的时候,如果其父亲还没有进行过初始化,则需要先触发其父类的初始化。(继承) 。接口除外
4.当虚拟机启动时候,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。 (启动类)
5.当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
被动引用:【被动使用不会引起里的初始化】
1.通过子类引用父类的静态字段,不会导致子类初始化。
class SuperClass{ static{ System.out.println("SuperClass init!"); } public static final int value = 123; } class ChildClass extends SuperClass{ static{ System.out.println("ChildClass init!"); } } public class ConstClass { public static void main(String[] args){ System.out.println(ChildClass.value); } } |
2.通过数组定义来引用类,不会触发此类的初始化。 (实际初始化的是数组类,这个是由JVM自动生成,直接继承Object的子类,创建动作由字节码指令newarray触发)
public class NotInitialization { /** * 被动引用类字段演示二: * 通过数组定义来引用类,不回触发此类的初始化。 * 没有输出结果,说明数组的定义不会引用类,触发初始化操作 */ public static void main(String[] args) { SuperClass[] sca = new SuperClass[10]; } } |
3.类常量在编译阶段会存入调用类的常量池(编译阶段的常量传播优化),本质上并无直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
/** * 被动引用类字段演示三: * 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化 * 结果仅仅输出 hello world ,而不会初始化ConstClass1 init! 静态代码块; * 分析:因为虽然在java源码中引用了ConstClass1类中的常量HELLOWORLD,但是在编译阶段将此常量的值存储到了NotInitialization1类的常量池中 * 实际上最后引用的是自己的常量池中的常量,并不会引入ConstClass1的加载。 */ class ConstClass1{ static{ System.out.println("ConstClass1 init!"); } public static final String HELLOWORLD = "hello world"; } public class NotInitialization1 { public static void main(String[] args) { System.out.println(ConstClass1.HELLOWORLD); } } |
4.接口的初始化:接口在初始化时,并不要求其父接口全部完成类初始化,只有在正使用到父接口的时候(如引用接口中定义的常量)才会初始化。(接口中没有static{ } 语句块,但编译器任然会为接口生成
接口和类的真正区别是接口是只有在正使用到父接口的时候(如引用接口中定义的常量)才会初始化
非数组类的加载可以通过系统提供的引导类加载器完成,也可由用户自定义类加载器完成,可以通过定义的类加载器去控制字节流的获取方式(即重写一个类加载器的loadClass()方法)
数组类本身不通过类加载器创建,由JVM直接创建的
数组类的创建过程遵循以下规则:
1.如果数组的组件类型(指的是数组去掉一个维度的类型)是引用类型,那就递归采用上面的加载过程去加载这个组件类型,数组c将加载该组件类型的类加载器的类名称空间上被标识(一个类必须和类加载器一起确定唯一性)
2.如果数组的组件类型不是引用类型(如int[]),Java虚拟机将会把数组c标识为与引导类加载器关联
3.数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public
类加载过程详解:
类的加载指将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构
类加载的过程:类从被加载到虚拟机内存中开始,到卸载出内存为止的过程:
整个生命周期包括:加载、链接、初始化。
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。
以下内容以HotSpot为基准。
1.加载(Loading):生成Class对象
获取类的二进制流,转为方法区数据结构,在Java堆中生成对应的java.lang.Class对象。【找到.class文件并把这个文件包含的字节码加载到内存中】,对于数组类来说,并没有对应的字节流,而是由JVM直接生成的。
在加载阶段(可参考java.lang.ClassLoader的loadClass()方法),虚拟机需要完成以下3件事情:
1、 通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可从其他渠道,如:网络(URLClassLoader)、动态生成、数据库等);
2、 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
3、 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;
对象肯定是存放在堆中的,但Class对象比较特殊,对于HotSpot虚拟机而言,Class对象是存放在方法区中的。
Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口(反射)
Class类无参构造函数为private。且只有java虚拟机才可以创建class类
链接:获取二进制字节流的方式?
1)从zip包中读取,最终成为日后JAR、EAR、WAR格式的基础
2)从网络中获取,典型的应用就是Applet
3)运行时计算生成,常见的是动态代理计技术。在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流
4)由其他文件生成,典型场景是JSP应用 。即由Jsp生成class类
5)从数据库中读取,这种场景相对少一些(中间件服务器)
加载和连接阶段(Linking)的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。
2.连接(Linking)
链接是指将创建成的类合并到JVM中,使之能执行的过程。
1)验证(Verification)
验证(安全的考虑,需要验证)为了保证class流的格式正确
为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
大致完成4个阶段的检验动作:文件格式验证,元数据验证,字节码验证,符号引用验证
文件格式验证:
文件格式验证:验证字节流是否符合Class文件格式的规范;
如:
1.是否以魔数开头:0xCAFEBABE
2.主、次版本号是否在当前虚拟机的处理范围中
3.常量池的常量中是否有不被支持的常量类型(检验常量tag标志)
4.指向常量中的各种索引值中是否有指向不存在的常量或不符合类型的常量
5.CONSTANT_Utf8_infoz型的常量中是否有不符合UTF8编码的数据
6.Class文件中各个部分以及文件本身是否有被删除的或附加的其他信息
…(还有很多)
【这个验证都是基于二进制字节流进行验证,只有通过类这个阶段的验证后,字节流才会进入内存的方法区进行存储,后面的3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流】
元数据(语义)验证:
元数据(语义)验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;
如:这个类是否有父类。Fianl类型的方法/类是否被覆盖,非抽象实现所有抽象方法。类中字段、方法是否与父类产生矛盾(如覆盖类父类的final字段,或出现不符合规则的方法重载,如方法参数都一致,但返回值类型却不同等)
1.是否有父类(除java.lang.Object外)
2.是否继承了不允许被继承的类(被final修饰的类)
3.如果不是抽象类,是否实现了父类中所有的抽象方法和接口中的所有方法
4.是否覆盖了父类的final字段或方法重载不符合规则
【主要目的是对类元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息】
字节码验证:
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
如运行检查。栈数据类型和操作码数据参数吻合,跳转指令到正确合理的位置。保证方法体中的类型转换时有效的(避免子类new父类)【对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件】
1.保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作(比如不会出现在操作数栈防了一个int型的数据却按long类型来加载到本地变量表中)
2.保证跳转指令不会跳到方法体外的字节码指令上
3.保证类型转化是有效的
但即使进行了大量的分析也不能保证字节码就是安全的,涉及到了著名的停机问题
由于数据流校验的高复杂性,耗时较大,JDK1.6后,在Javac中引入一项优化方法(可以通过参数-XX:-UseSplitVerifier关闭这种优化):在方法体的Code属性的属性表中增加一项“StackMapTable”属性,该属性描述了方法体中所有基本块开始时本地变量表和操作栈应有的状态,在字节码验证阶段,就不需要根据程序推到这些状态的合法性,只需要检查StackMapTable属性中的记录是否合法即可。从而将字节码验证的类型推导转变为类型检查从而节省一些时间。
JDK1.7后,对于主版本号大于50的class文件,使用类型检查来完成数据分析校验是唯一选择,不允许再退回到类型推倒的校验方式。
注:理论上StackMapTable存在错误或被篡改的可能,有可能code属性被修改了,然后StackMapTable也被篡改,欺骗jvm的校验。
符号引用验证:
符号引用(二进制兼容)验证:确保解析动作能正确执行。如work类引用了cat对象并调用其run方法,需判断是否有run方法,没有则抛出NoSuchMehodError异常。如常量池中描述类是否存在,访问的方法或字段是否存在足够的权限
检验内容有:
1.符号引用中通过字符串描述的全限定名是否能找到对应的类;
2.在指定类中是否存在符号方法的字段描述及简单名称所描述的方法和字段;
3.符号引用中的类、字段和方法的访问性(private、protected、public、default)是否可被当前类访问。
验证阶段非常重要,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,可考虑用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
2)准备(Preparation): 为静态变量分配内存并设置默认的初始值
正式为类变量分配内存并设置类变量初始值的阶段,这些变量所用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中(分配空间并进行默认值初始化)。【类变量在方法区。实例变量在堆区】
这里的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:
public static int value=123; |
变量value在准备阶段过后的初始值为0而不是123.因为这时尚未开始执行任何java方法,把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作在初始化阶段才会执行。
至于“特殊情况”是指:
public static final int value=123,
当类字段的属性是ConstantValue时,会在准备阶段初始化为指定的值,标注为final后,value的值在准备阶段初始化为123而非0.
3).解析(Resolution): 将符号引用替换为直接引用
虚拟机将常量池内的符号引用(也就是字符串)替换为直接引用(指针或者地址偏移量)的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
虚拟机规范中并为规定解析阶段发生的具体时间,只要求了再执行Java 虚拟机指令 anewarray、checkcast、getfield、getstatic、instanceof、nvokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield 和 putstatic 这16个用于操作符号的引用的字节码指令之前,先对他们所使用的符号引用进行解析。也就是执行上述任何一条指令都需要对它的符号引用的进行解析。所以:虚拟机实现可以根据需要来判断到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才取解析它。
对同一个符号引用进行多次解析请求是很常见的事情,虚拟机实现可以对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标识为已解析状态)从而避免解析动作重复进行。
对于invokedynamic指令,上面规则则不成立。当碰到某个前面已经由invokedynamic指令触发过解析的符号引用时,并不意味着这个解析结果对其他invokedynamic指令也同样生效。因为invokedynamic指令是JDK1.7新加入的指令,目的用于动态语言支持,它所对应的引用称为“动态调用点限定符”(Dynamic Call Site Specifier),这里“动态”的含义就是必须等到程序实际运行到这条指令的时候,解析动作才能进行。相对的,其余可触发解析的指令都是“静态”的,可以在刚刚完成加载阶段,还没有执行代码时就进行解析。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号进行引用,下面只对前4种引用的解析过程进行介绍,对于后面3种与JDK1.7新增的动态语言支持息息相关
类或接口的解析
假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,虚拟机完成整个解析的过程需要以下3个步骤:
1. 如果C不是一个数组类型,虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。在加载过程中,由于元数据验证、字节码验证的需要,又可能触发其他相关类的加载动作,例如加载这个类的父类或实现的接口。一旦这个加载过程出现了任何异常,解析过程就宣告失败。
2. 如果C是一个数组类型,并且数组的元素类型为对象,也就是N的描述符会是类似“[Ljava/lang/Integer”的形式,那将会按照第1点的规则加载数组元素类型。如果N的描述符如前面所假设的形式,需要加载的元素类型就是“java.lang.Integer”,接着由虚拟机生成一个代表此数组维度和元素的数组对象。
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的接口和父类中,或者同时在自己或父类的多个接口中出现,那编译器将可能拒绝编译。在下面代码示例中,如果注释了Sub类中的“public static int A=4; ”,接口与父类同时存在字段A,那编译器将提示“The field Sub.A is ambiguous”,并且拒绝编译这段代码。
public class FieldResolution {
interface Interface0 {
int A = 0;
}
interface Interface1 extends Interface0 {
int A = 1;
}
interface Interface2 {
int A = 2;
}
static class Parent implements Interface1 {
public static int A = 3;
}
static class Sub extends Parent implements Interface2 {
public static int A = 4;
}
public static void main(String[] args) {
System.out.println(Sub.A);
}
}
类方法解析
类方法解析的第一个步骤与字段解析一样,也需要先解析出类方法表的class_index项中索引的方法所属的类或接口的符号引用,如果解析成功,我们依然用C表示这个类,接下来虚拟机将会按照如下步骤进行后续的类方法搜索。
1. 类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。
2. 如果通过了第1步,在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
3. 否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
4. 否则,在类C实现的接口列表及他们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类C是一个抽象,这时查找结束,抛出java.lang.AbstractMethodError异常。
5. 否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError。
最后,如果查找过程成功返回了直接引用,将会对这个方法进行权限验证,如果发现不具备对此方法的访问权限,将抛出java.lang.IllegalAccessError异常。
接口方法解析
接口方法也需要先解析出接口方法表的class_index项中索引的方法所属的类或接口的符号引用,如果解析成功,依然用C表示这个接口,接下来虚拟机将会按照如下步骤进行后续的接口方法搜索。
1. 与类方法解析不同,如果在接口方法表中发现class_index中的索引C是个类而不是接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。
2. 否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
3. 否则,在接口C的父接口中递归查找,直到java.lang.Object(查找范围会包括Object类)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
4. 否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError异常。
5. 由于接口中的所有方法默认都是public,所以不存在访问权限的问题,因此接口方法的符号解析应当不会抛出java.lang.IllegalAccessError异常。
关于符号引用和直接引用:
1)符号引用(Symbolic References):
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
2)直接引用(Direct References):
直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接点位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。
解读一下:就拿以上截图的红色框框的例子来举例吧,框住的常量池语意大概是常量池中的第三个常量为类或接口的符号引用,这个符号的值为第四个常量池的值,也就是“java/lang/Object;”这是我们熟知的Object类的全限定名。解析阶段就是要把这个“class”的字符引用换成直接指向这个Object类在内存中的地址(如指针 )。那就说明,这个Object类必须同时也需要加载到内存中来。
3.初始化(Initialization):真正执行Jaca代码
【类中静态属性和初始化赋值、静态块的执行等】
初始化阶段,才真正开始执行类中定义的java程序代码。初始化阶段是执行类构造器
静态变量的初始化由两种途径:
1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;
2)如果类中存在初始化语句,就依次执行这些初始化语句
在连接的准备阶段,类变量已赋过一次系统要求的初始值,在初始化阶段,则是根据逻辑去初始化类变量和其他资源,如下:
public static int value1 = 5;
public static int value2 = 6;
static{
value2 = 66;
}
在准备阶段value1和value2都等于0;
在初始化阶段value1和value2分别等于5和66;
编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量;定义在它之后的变量,在前面的静态语句块可赋值,但不能访问
非法前向引用: public static int a = b; // 报错,非法前向引用。交换顺序就不会爆错了 // 非法向前引用 // static int i=1; static { i=0; System.out.println(i);//这句编译器会报错:Cannot reference a field before it is defined(非法向前引用)。注释掉就不会报错了,i结果是1 } static int i=1; |
初始化阶段是执行类构造器
所有类变量初始化语句和静态代码块都会在编译时被编译器放在收集器里头,存放到一个特殊的方法
子类的
()方法与实例构造器
由于父类的
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成
static class DeadLoopClass{
static {
if(true){
System.out.println(Thread.currentThread()+"init DeadLoopClass");
while(true){
}
}
}
}
public static void main(String[] args) {
Runnable script = new Runnable(){
public void run() {
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
需要注意的是,其他线程虽然会被阻塞,但如果执行()方法的那条线程退出()方法后,其他线程唤醒之后不会再次进入()方法。同一个类加载器下,一个类型只会初始化一次。
几种方法获取Class对象:
调用对象的getClass方法。
调用Class.forName()方法,参数为类的全名
使用.class属性获取Class对象
JVM类加载器机制与类加载过程
Java虚拟机启动、加载类过程分析
package org.luanlouis.jvm.load;
import sun.security.pkcs11.P11Util;
public class Main{
public static void main(String[] args) {
System.out.println("Hello,World!");
ClassLoader loader = P11Util.class.getClassLoader();
System.out.println(loader);
}
}
命令行下输入: java org.luanlouis.jvm.load.Main
当输入上述的命令时: java.exe 程序将完成以下步骤:
1. 根据JVM内存配置要求,为JVM申请特定大小的内存空间;
2. 创建一个引导类加载器实例,初步加载系统类到内存方法区区域中;
3. 创建JVM 启动器实例 Launcher,并取得类加载器ClassLoader;
4. 使用上述获取的ClassLoader实例加载我们定义的 org.luanlouis.jvm.load.Main类;
5. 加载完成时候JVM会执行Main类的main方法入口,执行Main类的main方法;
6. 结束,java程序运行结束,JVM销毁。
1.根据JVM内存配置要求,为JVM申请特定大小的内存空间
JVM启动时,JVM内存按功能划分,粗略地划分为方法区(Method Area) 和堆(Heap),所有的类的定义信息都会被加载到方法区中。
2.创建一个引导类加载器实例,初步加载系统类到内存方法区区域中;
JVM申请好内存空间后,JVM会创建一个引导类加载器(Bootstrap Classloader)实例,负责加载JVM虚拟机运行时所需的基本系统级别的类,如java.lang.String, java.lang.Object等等。
Bootstrap Classloader会读取 {JRE_HOME}/lib 下的jar包和配置,将这些系统类加载到方法区内。
可使用参数 -Xbootclasspath 或 系统变量sun.boot.class.path来指定的目录来加载类。
一般,{JRE_HOME}/lib下存放着JVM正常工作所需要的系统类,如下表所示:
文件名 |
描述 |
rt.jar |
运行环境包,rt即runtime,J2SE 的类定义都在这个包内 |
charsets.jar |
字符集支持包 |
jce.jar |
一组包,提供加密、密钥生成和协商以及 Message Authentication Code(MAC)算法的框架和实现 |
jsse.jar |
安全套接字拓展包Java(TM) Secure Socket Extension |
classlist |
该文件内表示是引导类加载器应该加载的类的清单 |
net.properties |
JVM 网络配置信息 |
Bootstrap ClassLoader加载系统类后,JVM内存会呈现如下格局:
引导类加载器将类信息加载到方法区中,以特定方式组织,对某一个特定的类而言,在方法区中应该有运行时常量池、类型信息、字段信息、方法信息、类加载器的引用,对应class实例的引用等信息。
类加载器的引用:由于这些类由引导类加载器(Bootstrap Classloader)加载的,Bootstrap是C++实现,无法访问,故而该引用为NULL
对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的实例放到堆(Heap)中, 作为访问方法区中类定义的入口和切入点。
测试:在代码中尝试获取系统类如java.lang.Object的类加载器时,你会始终得到NULL:
System.out.println(String.class.getClassLoader());//null
System.out.println(Object.class.getClassLoader());//null
System.out.println(Math.class.getClassLoader());//null
System.out.println(System.class.getClassLoader());//null
3.创建JVM 启动器实例 Launcher,并取得类加载器ClassLoader
上述步骤完成,JVM基本运行环境就准备就绪了。
要让JVM工作:运行我们定义的程序 org.luanlouis,jvm.load.Main。
此时,JVM虚拟机调用已经加载在方法区的类sun.misc.Launcher 的静态方法getLauncher(), 获取sun.misc.Launcher 实例:
sun.misc.Launcher launcher = sun.misc.Launcher.getLauncher(); //获取Java启动器
ClassLoader classLoader = launcher.getClassLoader(); //获取类加载器ClassLoader用来加载class到内存来
sun.misc.Launcher 单例,保证一个JVM虚拟机内只有一个sun.misc.Launcher实例。
Launcher内部,定义了两个类加载器(ClassLoader),
分别是sun.misc.Launcher.ExtClassLoader(拓展类加载器(Extension ClassLoader))和sun.misc.Launcher.AppClassLoader(应用类加载器(Application ClassLoader))
指向引导类加载器的虚线表示类加载器的这个有限的访问 引导类加载器 的功能。
除了Bootstrap ClassLoader的所有类加载器,都能判断某一个类是否被引导类加载器加载过,如果加载过,直接返回对应的Class
launcher.getClassLoader() 会返回 AppClassLoader 实例,AppClassLoader将ExtClassLoader作为父加载器。
4.使用类加载器加载Main类
通过 launcher.getClassLoader()方法返回AppClassLoader实例,
接着就是AppClassLoader加载 org.luanlouis.jvm.load.Main类的时候了。
ClassLoader classloader = launcher.getClassLoader();//取得AppClassLoader类
classLoader.loadClass("org.luanlouis.jvm.load.Main");//加载自定义类
定义的org.luanlouis.jvm.load.Main类被编译成class二进制文件,这个class文件中有一个叫常量池(Constant Pool)的结构体来存储该class的常量信息。
常量池中有CONSTANT_CLASS_INFO类型的常量,表示该class中声明了要用到那些类:
当AppClassLoader要加载 org.luanlouis.jvm.load.Main类时,会去查看该类的定义,发现它内部声明使用了其它的类: sun.security.pkcs11.P11Util、java.lang.Object、java.lang.System、java.io.PrintStream、java.lang.Class;
org.luanlouis.jvm.load.Main类要想正常工作,首先要能够保证这些其内部声明的类加载成功。
AppClassLoader要先将这些依赖类加载到内存。(注:为理解方便,没有考虑懒加载情况,事实上的JVM加载类过程比这复杂的多)
加载过程(双亲委派模式):
1. 加载java.lang.Object、java.lang.System、java.io.PrintStream、java,lang.Class
AppClassLoader尝试加载这些类的时候,会先委托ExtClassLoader进行加载;
ExtClassLoader发现不是其加载范围,其返回null;
AppClassLoader发现父类加载器ExtClassLoader无法加载,会查询这些类是否已经被BootstrapClassLoader加载过,结果表明这些类已经被BootstrapClassLoader加载过,则无需重复加载,直接返回对应的Class
2. 加载sun.security.pkcs11.P11Util。在{JRE_HOME}/lib/ext/sunpkcs11.jar包内,属于ExtClassLoader负责加载的范畴。
AppClassLoader尝试加载这些类的时候,会先委托ExtClassLoader进行加载;
ExtClassLoader发现其正好属于加载范围,故ExtClassLoader负责将其加载到内存中。ExtClassLoader在加载sun.security.pkcs11.P11Util时也分析这个类内都使用了哪些类,并将这些类先加载内存后,才开始加载sun.security.pkcs11.P11Util,加载成功后直接返回对应的Class
3. 加载org.luanlouis.jvm.load.Main
AppClassLoader尝试加载这些类的时候,先委托ExtClassLoader进行加载;ExtClassLoader发现不是其加载范围,其返回null;AppClassLoader发现父类加载器ExtClassLoader无法加载,则会查询这些类是否已经被BootstrapClassLoader加载过。而结果表明BootstrapClassLoader 没有加载过它,这时AppClassLoader只能自己动手负责将其加载到内存中,然后返回对应的Class
以上三步骤都成功,才表示classLoader.loadClass("org.luanlouis.jvm.load.Main")完成,
上述操作完成后,JVM内存方法区的格局会如下所示:
JVM方法区的类信息区是按照类加载器进行划分的,每个类加载器会维护自己加载类信息;
某个类加载器在加载相应的类时,会相应地在JVM内存堆(Heap)中创建一个对应的Class
5. 使用Main类的main方法作为程序入口运行程序
6. 方法执行完毕,JVM销毁,释放内存
关于类加载器
类加载器主要通过一个类的全限定名来获取描述此类的二进制字节流。
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
类加载器种类及其组织结构
类加载器(Class Loader):指的是可加载类的工具。
JVM自身定义了三个类加载器:
引导类加载器(Bootstrap Class Loader)、
拓展类加载器(Extension Class Loader )、
应用加载器(Application Class Loader)。
1. 引导类加载器(Bootstrap Class Loader)
使用C/C++底层代码实现的加载器,用以加载JVM运行时所需要的系统类,这些系统类在{JRE_HOME}/lib目录下。由于类加载器是使用平台相关的底层C/C++语言实现的,所以该加载器不能被Java代码访问到。但是可查询某个类是否被引导类加载器加载过。【负责加载JVM基础核心类库(rt.jar)】
经常使用的系统类如:java.lang.String,java.lang.Object,java.lang*....... 这些都被放在 {JRE_HOME}/lib/rt.jar包内, 当JVM系统启动的时候,引导类加载器会将其加载到 JVM内存的方法区中。
用Bootstrcp ClassLoader来加载自定义类,有两种方式:
1、在jvm中添加-Xbootclasspath参数,指定Bootstrcp ClassLoader加载类的路径,并追加我们自已的jar(ClassTestLoader.jar)
2、将class文件放到JAVA_HOME/jre/classes/目录下(上面有提到)
2. 拓展类加载器(Extension Class Loader)
加载 java 的{JRE_HOME}/lib/ext/ 目录下的拓展类 ,用来提供除了系统类外的额外功能。是整个JVM加载器的Java代码可访问到的类加载器的最顶端,即超级父加载器,拓展类加载器是没有父类加载器的。
3. 应用类加载器(Applocatoin Class Loader)
用于加载用户代码,是用户代码的入口。
经常执行指令 java xxx.x.xxx.x.x.XClass , 实际上,JVM就是使用的AppClassLoader加载 xxx.x.xxx.x.x.XClass 类的。应用类加载器将拓展类加载器当成自己的父类加载器,当其尝试加载类的时候,首先尝试让其父加载器-拓展类加载器加载;如果拓展类加载器加载成功,则直接返回加载结果Class
4. 用户自定义类加载器(Customized Class Loader):
用户可自己定义类加载器来加载类。所有的类加载器都要继承java.lang.ClassLoader类。
关系:ExtClassLoader 和 AppClassLoader 都继承URLClassLoader 类,而URLClassLoader又实现了抽象类ClassLoader,在创建Launcher对象时首先创建ExtClassLoader,然后将ExtClassLoader对象作为父加载器创建AppClassLoader对象,而通过Launcher.getClassLoader()方法获取的ClassLoader就是AppClassLoader对象。所以如果在Java应用中没有定义其他ClassLoader,那么除了 System.getProperty("java.ext.dirs”)目录下的类是由ExtClassLoader加载外,其他类都由AppClassLoader来加载
双亲委派模型(parent-delegation model):
双亲委派模式加载类:
当AppClassLoader加载类时,会先尝试让父加载器ExtClassLoader进行加载,
如果父加载器ExtClassLoader加载成功,则AppClassLoader直接返回父加载器ExtClassLoader加载的结果;
如果父加载器ExtClassLoader加载失败,AppClassLoader则会判断该类是否是通过Bootstrap类加载器加载,会调用native方法进行查找;
若要加载的类不是系统引导类,AppClassLoader将尝试自己加载,加载失败将会抛出“ClassNotFoundException”。
AppClassLoader的工作流程如下所示:(双亲委派模型)
这是JDK自身默认的加载类的行为,可通过继承复写方法,改变其行为。
对于某个特定的类加载器而言,应该为其指定一个父类加载器,当用其进行加载类的时候:
(双亲委派模式流程总结)
1. 委托父类加载器帮忙加载;
2. 父类加载器加载不了,查询引导类加载器有没有加载过该类;
3. 如果引导类加载器没有加载过该类,则当前的类加载器应该自己加载该类;
4. 若加载成功,返回对应的Class
总结:只有当父加载器反馈自己无法完成加载请求时,子加载器才会尝试自己加载。
注意:
双亲委派模型中的"双亲"并不是指它有两个父类加载器,一个类加载器只应该有一个父加载器。
而是有两个角色:
1. 父类加载器(parent classloader):可替子加载器尝试加载类(真爸爸)
2. 引导类加载器(bootstrap classloader): 子类加载器只能判断某个类是否被引导类加载器加载过,而不能委托它加载某个类(干爹);就是子类加载器不能接触到引导类加载器,引导类加载器对其他类加载器而言是透明的。
ClassLoader使用双亲委托模型来搜索类的,每个ClassLoader实例有一个父类加载器的引用(不是继承的关系,是包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可用作其它ClassLoader实例的的父类加载器。
好处:
1.java类随着它的加载器一起举杯了一种带有优先级的层次关系,如Object,存在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶层的启动类加载器进行加载的,因此Object在程序的各种类加载器环境中都是同一个类,如果不是双清模式,就可能出现多个不同的Object类,java体系最基础的行为也无法保证。
2.避免重复加载:当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次(杜绝冒充)
坏处:
如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱
Q:为什么要使用双亲委托这种模型呢?(防止多次载入)
这样可避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。如果不使用这种委托模式,就可随时使用自定义的String或Object来动态替代java核心api中定义的类型,就会存在非常大的安全隐患,双亲委托的方式可避免这种情况,因为String/Object已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,用户自定义的ClassLoader永远也无法加载一个自己写的String/Object,除非改变JDK中ClassLoader搜索类的默认算法。
双亲模式是默认的模式,但是不是必须这样做。如:Tomcat的WebappClassLoader会先加载自己的class,找不到再委托父类
Q:JVM在搜索类的时候,如何判定两个class是相同的呢?
JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足,JVM才认为这两个class是相同的。
就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。
双亲加载模型的逻辑和底层代码实现:(JDK1.8)
java.lang.ClassLoader的核心方法 loadClass()的实现:
先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器,如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载
//提供class类的二进制名称表示,加载对应class,加载成功,则返回表示该类对应的Class
public abstract class ClassLoader {
public Class> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); }
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException{ synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded,如果已经被加载,直接返回对应的Class实例 Class> c = findLoadedClass(name); if (c == null) { // 初次加载 long t0 = System.nanoTime();
// 双亲委托 try { if (parent != null) { //如果有父类加载器,则先让父类加载器加载 c = parent.loadClass(name, false); } else {// 没有父加载器(Extension ClassLoader),则查看是否已经被引导类加载器加载,有则直接返回 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { }
// 父加载器加载失败,并且没有被引导类加载器加载,则尝试该类加载器自己尝试加载 if (c == null) { // If still not found, then invoke findClass in order to find the class. long t1 = System.nanoTime(); c = findClass(name); // 自己尝试加载 // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) {//是否解析类 resolveClass(c); } return c; } }
protected final Class> findLoadedClass(String name) { if (!checkName(name)) return null; return findLoadedClass0(name); }
private native final Class> findLoadedClass0(String name);
private boolean checkName(String name) { if ((name == null) || (name.length() == 0)) return true; if ((name.indexOf('/') != -1) || (!VM.allowArraySyntax() && (name.charAt(0) == '['))) return false; return true; }
private Class> findBootstrapClassOrNull(String name){ if (!checkName(name)) return null; return findBootstrapClass(name); } private native Class> findBootstrapClass(String name);
protected final void resolveClass(Class> c) { resolveClass0(c); } private native void resolveClass0(Class> c);
loadClass用于实现类加载器间的架构
findClass用于根据name寻找字节流,并调用defineClass将字节流转换为Class
类加载器与Class
类加载器ClassLoader:
ClassLoader抽象类负责将Class加载到JVM中,审查每个类应该由谁加载,是一种父优先的等级加载机制(双亲委派模型),还有一个任务就是将Class字节码重新解析成JVM统一要求的对象格式
ClassLoader可定制,满足不同的字节码流获取方式
ClassLoader负责类装载过程中的加载阶段
defineClass():将byte字节流解析成JVM能够识别的class对象。可通过class文件实例化对象,还可通过其他方式实例化对象,如通过网络接收到一个类的字节码,拿这个字节码流直接创建类的Class对象形式实例化对象。
如果直接调用这个方法生成类的Class对象,这个类的Class对象还没有resolve,这个resolve将会在这个对象真正实例化时才进行。
defineClass通常是和findClass方法一起使用的,通过直接覆盖ClassLoader父类的fmdClass方法来实现类的加载规则,从而取得要加载类的字节码。然后调用defineClass方法生成类的Class对象,如果想在类被加载到JVM中时就被链接(Link),那么可接着调用另外一个resolveClass方法,当然也可选择让JVM来解决什么时候才链接这个类。
可用this.getClass().getClassLoader().loadClass(“class”)调用ClassLoader的loadClass方法获取这个类的Class对象。
几个关键方法:
(1)loadClass : 此方法负责加载指定名字的类,ClassLoader的实现方法为先从已经加载的类中寻找,如没有则继续从parent ClassLoader中寻找,如仍然没找到,则从System ClassLoader中寻找,最后再调用findClass方法来寻找,如要改变类的加载顺序,则可覆盖此方法
(2)findLoadedClass : 此方法负责从当前ClassLoader实例对象的缓存中寻找已加载的类,调用的为native的方法。
(3)findClass : 此方法直接抛出ClassNotFoundException,因此需要通过覆盖loadClass或此方法来以自定义的方式加载相应的类。
(4)findSystemClass : 此方法负责从System ClassLoader中寻找类,如未找到,则继续从Bootstrap ClassLoader中寻找,如仍然为找到,则返回null。
(5)defineClass : 此方法负责将二进制的字节码转换为Class对象
(6)resolveClass : 此方法负责完成Class对象的链接,如已链接过,则会直接返回。
自定义ClassLoader:
不管是直接实现抽象类ClassLoader,还是继承URLClassLoader类,或其他子类,它的父加载器都是AppClassLoader,因为不管调用哪个父类构造器,创建的对象都必须最终调用getSystemClassLoader()作为父加载器。而getSystemClassLoader()方法获取到的正是 AppClassLoader。
为什么还要自定义类加载器呢?
Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果想加载其它位置的类或jar时,如:要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现自己的业务逻辑。这样的情况下,默认的ClassLoader就不能满足需求,需要定义自己的ClassLoader。
定义自已的类加载器分为两步:
1、继承java.lang.ClassLoader
2、重写父类的findClass方法,根据参数指定类的名字,返回对应的Class对象的引用
JDK已经在loadClass方法中实现了ClassLoader搜索类的算法,当在loadClass方法中搜索不到类时,loadClass方法会调用findClass方法来搜索类,只需重写该方法即可。没有特殊要求,不建议重写loadClass搜索类的算法。
ClassLoader的loadClass方法:
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException
name:该类的 binary name 、resolve:如果 true然后解析该类
加载指定的类别binary name 。 此方法的默认实现按以下顺序搜索类: (ClassLoader默认搜索算法·)
1、调用findLoadedClass(String)以检查类是否已经加载。
2、在父类加载器上调用loadClass方法。 如果父级是null ,则使用内置到虚拟机中的类加载器。
3、调用findClass(String)方法来查找类。
如果使用上述步骤找到该类,并且resolve标志为真,则该方法将调用resolveClass(Class)方法对所生成的Class对象。
鼓励ClassLoader子类覆盖findClass(String) ,而不是这种方法。
除非被覆盖,否则该方法在整个类加载过程中同步getClassLoadingLock方法的结果。
自定义加载类:
public class NetworkClassLoader extends ClassLoader {
private String rootUrl;
public NetworkClassLoader(String rootUrl) {
this.rootUrl = rootUrl;
}
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
Class clazz = null;//this.findLoadedClass(name); // 父类已加载
//if (clazz == null) { //检查该类是否已被加载过
byte[] classData = getClassData(name); //根据类的二进制名称,获得该class文件的字节码数组
if (classData == null) throw new ClassNotFoundException();
clazz = defineClass(name, classData, 0, classData.length); //将class的字节码数组转换成Class类的实例
//}
return clazz;
}
private byte[] getClassData(String name) {
InputStream is = null;
try {
String path = classNameToPath(name);
URL url = new URL(path);
byte[] buff = new byte[1024*4];
int len = -1;
is = url.openStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while((len = is.read(buff)) != -1) {
baos.write(buff,0,len);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch(IOException e) {
e.printStackTrace();
}
}
}
return null;
}
private String classNameToPath(String name) {
return rootUrl + "/" + name.replace(".", "/") + ".class";
}
}
测试类:
try {
String rootUrl = "http://localhost:8080/httpweb/classes";
NetworkClassLoader networkClassLoader = new NetworkClassLoader(rootUrl);
String classname = "org.classloader.simple.NetClassLoaderTest";
Class clazz = networkClassLoader.loadClass(classname);
System.out.println(clazz.getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
常用web服务器中都定义了自己的类加载器,用于加载web应用指定目录下的类库(jar或class),如:Weblogic、Jboss、tomcat等,
双亲委派模式的被破坏
双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK 1.2发布之前。由于双亲委派模型在JDK 1.2之后才被引入,而类加载器和抽象类java.lang.ClassLoader则在JDK 1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。为了向前兼容,JDK 1.2之后的java.lang.ClassLoader添加了一个新的protected方法findClass(),在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。
双亲委派的具体逻辑就实现在loadClass()方法之中,JDK 1.2后已不提倡用户再去覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里如果父类加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派规则的。
双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷所导致的,双亲委派很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),但基础类想要调用用户类的代码如何实现?。可以通过JNDI服务,它的代码由启动类加载器加载,JNDI的目的就是对资源进行集中管理和查找。需要调用独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI、Service Provider Interface)代码。但启动类加载器不认识这些代码。通过线程上下文加载器去解决。JNDI服务通过使用线程上下文类加载器去加载所需要的SPI代码。也就是父类加载器请求子类加载器去完成类加载动作。打破了双清委派结构来逆向使用类加载器。
Java中所有涉及SPI的加载动作都采用这种方式,如JNDI、JDBC、JCE、JAXB和JBI等
线程上下文加载器
Java 任何一段代码的执行,都有对应的线程上下文。如果在代码中,想看当前是哪一个线程在执行当前代码
使用如下方法:
Thread thread = Thread.currentThread();//返回对当当前运行线程的引用
可为当前的线程指定类加载器。当执行 java org.luanlouis.jvm.load.Main时,JVM会创建一个Main线程,而创建应用类加载器AppClassLoader的时候,会将AppClassLoader设置成Main线程的上下文类加载器:
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
//将AppClassLoader设置成当前线程的上下文加载器
Thread.currentThread().setContextClassLoader(this.loader);
//.......
}
线程上下文类加载器是从线程的角度来看待类的加载,为每一个线程绑定一个类加载器,可将类的加载从单纯的双亲加载模型解放出来,进而实现特定的加载需求。
双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的, “动态性”指代码热替换(HotSwap)、模块热部署(Hot Deployment)等,
目前OSGi已经成为了业界“事实上”的Java模块化标准,而OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(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)否则,类查找失败。
上面的查找顺序中只有开头两点仍然符合双亲委派规则,其余的类查找都是在平级的类加载器中进行的。
OSGi中对类加载器的使用是很值得学习的,弄懂了OSGi的实现,就可以算是掌握了类加载器的精髓。
热部署与热加载
在应用运行的时升级软件,无需重新启动的方式有两种,热部署和热加载。
对于Java应用程序来说,热部署就是在服务器运行时重新部署项目,热加载即在在运行时重新加载class,从而升级应用。
实现原理:热加载的实现原理主要依赖java的类加载机制,在实现方式可概括为在容器启动的时候起一条后台线程,定时的检测类文件的时间戳变化,如果类的时间戳变掉了,则将类重新载入。
热加载与反射:
对比反射机制,反射是在运行时获取类信息,通过动态的调用来改变程序行为; 热加载是在运行时通过重新加载改变类信息,直接改变程序行为。
热部署原理类似,但它是直接重新加载整个应用,这种方式会释放内存,比热加载更加干净彻底,但同时也更费时间。
原理:一个类加载器在运行期只能加载同一个类一次,无论该类是否被修改,所以需要新建类加载来加载同一个类,引用重定向了旧的加载器实例被抛弃。
JAVA热部署(hotswap)实现
在不重启 Java 虚拟机的前提下,能自动侦测到 class 文件的变化,更新运行时 class 的行为。
Java 类是通过 Java 虚拟机加载的,某个类的 class 文件在被 classloader 加载后,会生成对应的 Class 对象,之后就可创建该类的实例。
默认的虚拟机行为只会在启动时加载类,如果后期有一个类需要更新的话,单纯替换编译的 class 文件,Java 虚拟机是不会更新正在运行的 class。
如果要实现热部署,有两种方法:
1.修改虚拟机的源代码,根本上改变 classloader 的加载行为,使虚拟机能监听 class 文件的更新,重新加载 class 文件,这样的行为破坏性很大,为后续的 JVM 升级埋下了一个大坑。
2.创建自己的 classloader 来加载需要监听的 class,这样就能控制类加载的时机,从而实现热部署。
java类的加载过程:
一个java类文件到虚拟机里的对象,要经过如下过程:首先通过java编译器将java文件编译成class字节码,类加载器读取class字节码,再将类转化为实例,对实例newInstance就可生成对象。
类加载器ClassLoader功能,也就是将class字节码转换到类的实例。在java应用中,所有的实例都是由类加载器,加载而来。
一般在系统中,类的加载都是由系统自带的类加载器完成,而且对于同一个全限定名的java类(如com.csiar.soc.HelloWorld),只能被加载一次,而且无法被卸载。
希望将java类卸载,并且替换更新版本的java类,怎么做?把类加载器换了,用自定义的替代。
实现热部署步骤:
1、销毁该自定义ClassLoader ,并重写ClassLoader的findClass方法(被该加载器加载的class也会自动卸载)
2、更新class类文件
3、创建新的ClassLoader去加载loadClass更新后的class类文件。
外部显示调用loadClass时。
1.此类之前加载过
1)期间Class重编译过:新建加载器重加载(该ClassLoader有一个静态引用给外部共享,热替换时新建对象实例)
2)期间Class没有重编译过:返回之前的class
2.此类之前没有加载过
核心类由系统加载器负责加载。用户自定义类由原自定义加载器加载
替换的结果是返回一个class对象由外部反射调用
JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):
- 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。
- 加载该类的ClassLoader已经被GC。
- 该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法
对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
代码实现
/** * 自定义类加载器,并override findClass方法 */
public class MyClassLoader extends ClassLoader{
@Override
public Class> findClass(String name) throws ClassNotFoundException{
try{
String fileName = name.substring(name.lastIndexOf("." )+1) + ".class" ;
InputStream is = this.getClass().getResourceAsStream(fileName);
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b. length);
} catch(IOException e){
throw new ClassNotFoundException(name);
}
}
}
package com.csair.soc.hotswap;
public class HelloWorld {
public void say(){
System. out.println( "Hello World V1");
}
}
public class HelloWorld {
public void say(){
System. out.println( "Hello World V2");
}
}
public class Hotswap {
public static void main(String[] args) throws Exception {
loadHelloWorld();
// 回收资源,释放HelloWorld.class文件,使之可被替换
System. gc();
Thread. sleep(1000);// 等待资源被回收
File fileV2 = new File( "HelloWorld.class");
File fileV1 = new File("bin\\com\\csair\\soc\\hotswap\\HelloWorld.class" );
fileV1.delete(); //删除V1版本
fileV2.renameTo(fileV1); //更新V2版本
System. out.println( "Update success!");
loadHelloWorld();
}
public static void loadHelloWorld() throws Exception {
MyClassLoader myLoader = new MyClassLoader(); //自定义类加载器
Class> class1 = myLoader.findClass( "com.csair.soc.hotswap.HelloWorld");//类实例
Object obj1 = class1.newInstance(); //生成新的对象
Method method = class1.getMethod( "say");
method.invoke(obj1); //执行方法say
System. out.println(obj1.getClass()); //对象
System. out.println(obj1.getClass().getClassLoader()); //对象的类加载器
}
}
输出结果:
Hello World V1
class com.csair.soc.hotswap.HelloWorld
com.csair.soc.hotswap.MyClassLoader@bfc8e0
Update success!
Hello World V2
class com.csair.soc.hotswap.HelloWorld
com.csair.soc.hotswap.MyClassLoader@860d49
根据结果可看到,在没有重启应用的情况下,成功的更新了HelloWorld类。
以上只是热部署的最简单的原理实践,实际情况会复杂的多。
OSGI的最关键理念就是应用模块(bundle)化,对于每一个bundle,都有其自己的类加载器,当要更新bundle时,把bundle和它的类加载器一起替换掉,就可实现模块的热替换
Tomcat类加载机制:
参考地址:https://www.cnblogs.com/aspirant/p/8991830.html
在tomcat中都是通过扩展URLClassLoader来实现自己的类加载器
对于JVM来说:
按这个过程,如果同样在CLASSPATH指定的目录中和自己工作目录中存放相同的class,会优先加载CLASSPATH目录中的文件。
双亲委派模式优点:
避免类加载混乱,将类分层次,如java中lang包下的类在jvm启动时就被启动类加载器加载了,而用户一些代码类则由应用程序类加载器(AppClassLoader)加载,基于双亲委托模式,就算用户定义了与lang包中一样的类,最终还是由应用程序类加载器委托给启动类加载器去加载,这个时候启动类加载器发现已经加载过了lang包下的类了,所以两者都不会再重新加载。如果使用者通过自定义的类加载器可以强行打破这种双亲委托模型,但也不会成功的,java安全管理器抛出将会抛出java.lang.SecurityException异常
1、Tomcat 不遵循双亲委派机制,如果自定义一个恶意的HashMap,是否有风险?(阿里)
不会有风险,如果有,Tomcat都运行这么多年了,那群Tomcat大神能不改进吗?
tomcat不遵循双亲委派机制,只是自定义的classLoader顺序不同,但顶层还是相同的,还是要去顶层请求classloader.
Tomcat作为web容器, 要解决什么问题:
1. 一个web容器可能要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
2. 部署在同一个web容器中相同的类库相同的版本可共享。否则,如果服务器有10个应用程序,有10份相同的类库加载进虚拟机。
3. web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应让容器类库和程序类库隔离开来。
4. web容器要支持jsp的修改,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情,否则要你何用? 所以,web容器需要支持 jsp 修改后不用重启(热加载)。
总结:共享类库、不同Web应用资源隔离、热加载。
Tomcat 如果使用默认的类加载机制行不行?
不行。
第一个问题,如果用默认类加载器机制,无法加载两个相同类库的不同版本,默认的累加器不管是什么版本,只在乎全限定类名,并且只有一份。
第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。
第三个问题和第一个问题一样。
第四个问题,jsp 文件其实也就是class文件,修改了,类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp不会重新加载。解决方案是直接卸载掉这jsp文件的类加载器,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。
Tomcat 如何实现自己独特的类加载机制?设计图:
前3个类加载和默认的一致,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader是Tomcat自己定义的类加载器,分别加载/common/*、/server/*、/shared/*(在tomcat 6之后已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/*中的Java类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。
•common Loader:Tomcat最基本的类加载器,加载路径中的class可被Tomcat容器本身以及各个Webapp访问;
•catalina Loader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
•shared Loader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但对于Tomcat容器不可见;
•Webapp ClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;
从图中的委派关系中可以看出:
CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,实现了公有类库的共用,而CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。
WebAppClassLoader可用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
JasperLoader的加载范围仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。
Common,Catalina,Shared类加载器是URLClassLoader类的一个实例,只是它们的类加载路径不一样,在tomcat/conf/catalina.properties配置文件中配置(common.loader,server.loader,shared.loader).WebAppClassLoader继承自WebAppClassLoaderBase,基本所有逻辑都在WebAppClassLoaderBase为中实现了, tomcat的所有类加载器都是以URLClassLoader为基础进行扩展
Common,Catalina,Shared类加载器是URLClassLoader类的一个实例,在默认的配置中,它们其实都是同一个对象,即commonLoader,结合初始化时的代码(只保留关键代码):
private void initClassLoaders() {
commonLoader = createClassLoader("common", null); // commonLoader的加载路径为common.loader
if( commonLoader == null ) {
commonLoader=this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", commonLoader); // 加载路径为server.loader,默认为空,父类加载器为commonLoader
sharedLoader = createClassLoader("shared", commonLoader); // 加载路径为shared.loader,默认为空,父类加载器为commonLoader
}
private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception {
String value = CatalinaProperties.getProperty(name + ".loader");
if ((value == null) || (value.equals("")))
return parent; // catalinaLoader与sharedLoader的加载路径均为空,所以直接返回commonLoader对象,默认3者为同一个对象
}
在上面的代码初始化时很明确是指出了,catalina与shared类加载器的父类加载器为common类加载器,而初始化commonClassLoader时父类加载器设置为null,最终会调到createClassLoader静态方法:
public static ClassLoader createClassLoader(List
.....
return AccessController.doPrivileged(
new PrivilegedAction
@Override
public URLClassLoader run() {
if (parent == null)
return new URLClassLoader(array); //该构造方法默认获取系统类加载器为父类加载器,即AppClassLoader
else
return new URLClassLoader(array, parent);
}
});
}
在createClassLoader中指定参数parent==null时,最终会以系统类加载器(AppClassLoader)作为父类加载器,这解释了为什么commonClassLoader的父类加载器是AppClassLoader.
一个web应用对应着一个StandardContext实例,每个web应用都拥有独立web应用类加载器(WebClassLoader),这个类加载器在StandardContext.startInternal()中被构造了出来:
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
getParentClassLoader()会获取父容器StandarHost.parentClassLoader对象属性,而这个对象属性是在Catalina$SetParentClassLoaderRule.begin()初始化,初始化的值其实就是Catalina.parentClassLoader对象属性,再来跟踪一下Catalina.parentClassLoader,在Bootstrap.init()时通过反射调用了Catalina.setParentClassLoader(),将Bootstrap.sharedLoader属性设置为Catalina.parentClassLoader,所以WebClassLoader的父类加载器是Shared ClassLoader.
Q:tomcat 违背了java 推荐的双亲委派模型。
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当由自己的父类加载器先加载。
Tomcat为了实现隔离性,没有遵守这个约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器。
Tomcat的类加载机制违反了双亲委托原则的,对于一些未加载的非基础类(Object,String等),各个web应用自己的类加载器(WebAppClassLoader)会优先加载,加载不到时再交给commonClassLoader走双亲委托。 具体加载逻辑位于WebAppClassLoaderBase.loadClass()方法中,过程大致如下:
1.先在本地缓存中查找是否已经加载过该类(对于一些已经加载了的类,会被缓存在resourceEntries这个数据结构中),如果已经加载即返回,否则 继续下一步。
2.让系统类加载器(AppClassLoader)尝试加载该类,主要是为了防止一些基础类会被web中的类覆盖,如果加载到即返回,返回继续。
3.前两步均没加载到目标类,那么web应用的类加载器将自行加载,如果加载到则返回,否则继续下一步。
4.最后还是加载不到的话,则委托父类加载器(Common ClassLoader)去加载。
3.4违背了双亲委派机制。
除了tomcat之外,JDBC,JNDI,Thread.currentThread().setContextClassLoader();等很多地方都一样是违反了双亲委托
Q:Tomcat 的 Common ClassLoader 想加载 WebApp ClassLoader 中的类,怎么办?
可使用线程上下文类加载器实现,使用线程上下文加载器,可以让父类加载器请求子类加载器去完成类加载的动作。
Tomcat的类加载器:
tomcat启动时,会创建几种类加载器:
1 Bootstrap 引导类加载器 :加载JVM启动所需的类,以及标准扩展类(位于jre/lib/ext下)
2 System系统类加载器:加载tomcat启动的类,如bootstrap.jar,通常在catalina.bat或catalina.sh中指定。CATALINA_HOME/bin下
3 Common 通用类加载器 :加载tomcat使用以及应用通用的一些类,位于CATALINA_HOME/lib下,如servlet-api.jar
4 webapp 应用类加载器:每个应用在部署后,都会创建一个唯一的类加载器。该类加载器会加载位于 WEB-INF/lib下的jar文件中的class 和 WEB-INF/classes下的class文件。
当应用需要到某个类时,则会按照下面的顺序进行类加载:
1 使用bootstrap引导类加载器加载
2 使用system系统类加载器加载
3 使用应用类加载器在WEB-INF/classes中加载
4 使用应用类加载器在WEB-INF/lib中加载
5 使用common类加载器在CATALINA_HOME/lib中加载
基础练习:
延伸出来问题进行分析:
public class SSClass{
static{
System.out.println("SSClass");
}
}
public class SuperClass extends SSClass{
static{
System.out.println("SuperClass init!");
}
public static int value = 123;
public SuperClass(){
System.out.println("init SuperClass");
}
}
public class SubClass extends SuperClass{
static{
System.out.println("SubClass init");
}
static int a;
public SubClass() {
System.out.println("init SubClass");
}
}
public class NotInitialization{
public static void main(String[] args){
System.out.println(SubClass.value);
}
}
运行结果:
SSClass
SuperClass init!
123
public class ConstClass{
static{
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";
}
public class NotInitialization{
public static void main(String[] args){
System.out.println(ConstClass.HELLOWORLD); // 运行结果只有HelloWorld
}
}
类加载小总结:
1, JVM会先去方法区中找有没有相应类的.class存在。如果有,就直接使用;如果没有,则把相关类的.class加载到方法区
2, 在.class加载到方法区时,会分为两部分加载:先加载非静态内容,再加载静态内容
3, 加载非静态内容:把.class中的所有非静态内容加载到方法区下的非静态区域内
4, 加载静态内容:
4.1、把.class中的所有静态内容加载到方法区下的静态区域内
4.2、静态内容加载完成之后,对所有的静态变量进行默认初始化
4.3、所有的静态变量默认初始化完成之后,再进行显式初始化
4.4、当静态区域下的所有静态变量显式初始化完后,执行静态代码块
5,当静态区域下的静态代码块,执行完之后,整个类的加载就完成了。