JVM把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这既是虚拟机的类加载机制。
与那些在编译时需要进行连接工作的语言(如C,C++)不同,Java语言里,类型的加载、连接和初始化都是在程序的运行期间完成的,Java里天生可以动态扩展的语言特性就是依赖运行期间动态加载和动态链接这个特点实现的。
类从被加载到虚拟机内存开始,到卸载出内存为止,他的整个生命周期包括:
什么时候需要开始类加载过程的第一阶段加载,虚拟机没有进行强制约束,但是对于初始化阶段,虚拟机规范严格规定了下面5中情况(有且只有)必须对类进行初始化的情况:
对于静态字段(static字段),只有直接定义这个字段的类才会初始化,因此通过其子类来引用父类的静态字段,只会触发父类的初始化不会触发子类的初始化
。下面的代码中子类Student调用父类中定义的静态int型变量static_num,但是Student类并没有初始化,仅初始化了父类Person。
//Peson
public class Person{
public static int static_num = 10;
static {
System.out.println("class init");
}
}
//Student
public class Student extends Person{
static {
System.out.println("subclass init");
}
}
//Test
public class Test {
public static void main(String[] args) throws InterruptedException {
System.out.println(Student.static_num);
}
}
//结果
//class init
//10
下面这段代码运行之后没有输出class init,说明没有触发com.jian8.basic.Person类的初始化。但是这段代码触发了另外一个名为“[Lcom.jian8.basic.Person”类的初始化。这是由虚拟机自动生成的、直接继承于java.lang.Object的子类,创建动作由字节码指令newarray触发。
这个类表明了一个元素类型为com.jian8.basic.Person的一维数组,数组中应有的属性和方法都实现在这个类里。Java语言中对数组的访问比C/C++安全就是因为这个类封装了数组元素的访问方法,而C/C++直接翻译为数组指针的移动。
//Peson
public class Person{
public static int static_num = 10;
static {
System.out.println("class init");
}
}
//Test
public class Test {
public static void main(String[] args) throws InterruptedException {
Person[] pa = new Person[10];
}
}
//没有输出
下面的代码运行后没有输出constclass init,这是因为虽然在Java源码中引用了ConstClass类中的常量static_str,但其实在偏移阶段通过常量传播优化,已经将此常量的值“HELLO_WORLD”存储到了Test类的常量池中,以后Test对常量池ConstClass.static_str的引用实际都转化为Test对自身常量池的引用。也就是说,实际上Test的Class文件之中并没有ConstClass类的符号引用入口,这两个类在编程成Class之后就不存在任何联系了。
class ConstClass{
{
System.out.println("constclass init");
}
public static final String static_str = "HELLO_WORLD";
}
public class Test {
public static void main(String[] args) throws InterruptedException {
System.out.println(ConstClass.static_str);
}
}
//结果
//HELLO_WORLD
加载时类加载过程的一个阶段。在加载阶段,虚拟机要完成以下3件事:
对于数组类而言,情况有所不同,数组本身不通过类加载器创建,它是由Java虚拟机直接创建的。但数组类与类加载器仍然有着密切的关系因为数组类的元素类型最终还是要考累加器区创建。
主要验证字节流是否符合Class文件格式的规范。如是否以魔数0xCAFEBABE开头,主次版本号是否在虚拟机的处理范围内等。
通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如保证跳转指令不会跳转到方法体意外的字节码指令上。
符号引用验证的目的是确保解析动作能正常执行。
准备阶段是正式为类变量分配内存并设置类初始值的阶段,这个变量所使用的内存都将在方法区中进行分配。首先,这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次,这个初始值通常情况下是数据类型的零值,假设一个类变量定义如下:
public static int value = 123;
那变量value在准备阶段后初始值是0,而不是123. 因为这时候还尚未开始执行任何Java方法,而把value赋值为123的putstatic指令时程序被编译后,存放于类构造器()方法之中,所以赋值123的动作要在初始化阶段才会执行。
假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或者接口C的直接引用
,那VM完成整个解析过程需要以下三个步骤:
1)如果C不是一个数组类型,那VM将会把代表N的全限定名传给D的类加载器去加载这个类C。在加载的过程中,由于元数据、字节码验证的需要,可能又会触发其他类的加载动作,例如这个类的父类或实现接口。
2)如果C是一个数组类型,并且数组的元素类型也为对象,也就是N的描述符回事类似Ljava/lang/Integer”的形式,那将会安装第一点的规则去加载数组元素类型。如果N的描述符如前面假设的形式,需要加载的元素类型是“java.lang.Integer”,接着有虚拟机生成个代表此数组维度和元素的数组对象。
3)如果上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为一个有效的类或者接口了,但在解析之前还要进行符号引号,确认D是否具备C的访问权限。
类方法解析的第一个步骤和字段解析一样,也需要先解析出类方法表的class_index项中索引的方法所属的类或接口的符号引用,如果解析成功,我们依然用C表示这个类,接下来VM将会安装如下步骤进行后续的类方法搜索:
确认索引的C是否是个接口,不是则继续;是则IncompatibleClassChangeError -》找类C -》找类C的父类 -》找类C实现的接口列表 -》方法查找失败,抛出java.lang.NoSuchMethodError
类初始化是类加载的最后一步。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据开发人员通过程序制定的计划去初始化变量和其他资源。初始化阶段执行的是类构造器
方法是由编译器自动收起类中所有类的变量的赋值动作和静态语句块(static{ }
)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量
,在前面的静态语句块可以赋值,但是不能访问,如下代码所示。
public class Test{
static{
//给变量赋值编译可以正常通过
i = 0;
//编译器会提示“非法向前引用变量”
System.out.print(i);
}
static int i = 0;
}