Java类的加载机制
1.jvm和类的关系
当调用java命令运行一个java程序时,必会启动一个jvm,该java程序的所有线程、变量都处于jvm中,都使用该jvm
的内存区。
jvm的终止情况:
- 程序自然运行结束;
- 程序执行过程中,遇到System.exit();Runtime.getRuntime.exit();
- 程序执行过程中,遇到未捕获的异常或错误时;
- 程序所在的平台强制结束了jvm进程。
2.类的加载过程
类的生命周期
类从被加载到虚拟机内存开始,到卸载出内存为止,它的生命周期如下所示,可划分为七个阶段:
加载、验证、准备、初始化和卸载这五个阶段顺序时固定的,类的加载过程必须按照这种顺序开始。但是解析阶段不一定,它在某种情况下,可以在初始化之后再开始,这是为了在运行时动态绑定某些特性(例如:接口只有在调用的时候才知道具体实现的是哪个子类)。这些阶段通常都是互相交叉混合式进行的,一个阶段执行的过程中往往会**另外一个阶段。
加载阶段
加载阶段是“类加载机制”中的一个阶段,亦称“装载”,主要完成:
- 通过“类全名”来获取定义此类的二进制字节流;
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构;
- 在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需要的格式存储在方法区中,方法区中的数据存储格式由虚拟机自行定义,虚拟机并未规定此区域的具体数据结构。然后在java堆中实例化一个java.lang.Class类的对象,这个对象作为程序访问方法区中这些数据结构的外部接口。
验证阶段
验证是链接阶段的第一步,这一步主要的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害到虚拟机的自身安全。
包括四个验证过程:
- 文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。这阶段的验证是基于二进制字节流进行的,只有通过这个阶段的验证后,字节流才会进入内存的方法区中进行存储,所以后面的3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。
- 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,主要目的是对类的元数据信息进行语义校检,保证不存在不符合Java语言规范的元数据信息。
- 字节码验证:主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型做完校验后,这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
- 符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在来链接的第三个阶段--解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常需要校验下列内容:符号引用中通过字符串描述的全限定名是否能找到对应的类;在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段;符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可以被当前类访问。
准备阶段
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区进行分配。注意:这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次,这里所说的“通常情况”下是数据类型的零值,假设一个类变量的定义为:
public static int value = 123;
变量value在准备阶段过后的初始值为0而不是123,因为把value赋值为123的putstatic指令是程序被编译后,存放在类构造器<client>()方法之中,所以赋值动作将会在初始化阶段才会执行,下表列出了Java中所有基本数据的零值。
数据类型 | 零 值 | 数据类型 | 零 值 |
int | 0 | boolean | false |
long | 0L | float | 0.0f |
short | (short)0 | double | 0.0d |
char | '\u0000' | reference | null |
byte | (byte)0 |
特殊情况:如果类字段的属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,假设上面类变量value的定义变为:
public static final int value = 123;
编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。
解析阶段
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定7类符号引用进行。
- 符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们接收的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
- 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必然已经在内存中存在。
初始化阶段
初始化阶段是类加载过程的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过制定的主观计划去初始化类变量和其他资源,初始化阶段是执行类构造器<client>()方法的过程,必须进行初始化的5中情况:
- 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令最常见的Java代码场景:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的字段除外)的时候,以及调用一个类的静态方法的时候。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个类。
- 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果RET_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法所对应的类没有进行过初始化,则需要先触发其初始化。