JVM把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这既是虚拟机的类加载机制。
与那些在编译时需要进行连接工作的语言(如C,C++)不同,Java语言里,类型的加载、连接和初始化都是在程序的运行期间完成的,Java里天生可以动态扩展的语言特性就是依赖运行期间动态加载和动态链接这个特点实现的。
类加载的时机
类从被加载到虚拟机内存开始,到卸载出内存为止,他的整个生命周期包括:
- 加载loading
- 验证verification
- 准备preparation
- 解析resolution
- 初始化initialization
- 使用using
- 卸载unloading
什么时候需要开始类加载过程的第一阶段加载,虚拟机没有进行强制约束,但是对于初始化阶段,虚拟机规范严格规定了下面5中情况(有且只有)必须对类进行初始化的情况:
- 遇到new、getstatic、putstatic、invokestaic这4条字节码指令时
- 使用java.lang.reflect包的方法对类进行反射调用的时候
- 当初始化一个类的时候,发现其父类还没有进行初始化,则先触发其父类的初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类(main方法的那个类),虚拟机会先初始化这个类
- 当使用动态语言支持时
被动引用例子1
对于静态字段(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
被动引用例子2
下面这段代码运行之后没有输出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];
}
}
//没有输出
被动引用例子3
下面的代码运行后没有输出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件事:
- 通过一个类的权限的名来获取定义此类的二进制字节流
- 从哪获取?
- ZIP,如jar,ear,war等等
- 网络中获取,Applet
- 运行时计算生成,动态代理技术
- 其他文件生成,JSP应用,由JSP文件生成对应的Class类
- 从数据库获取,如SAP Netweaver
- 从哪获取?
- 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
对于数组类而言,情况有所不同,数组本身不通过类加载器创建,它是由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程序代码(或者说是字节码)。
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据开发人员通过程序制定的计划去初始化变量和其他资源。初始化阶段执行的是类构造器<clinit()>方法的过程。这里我们看一下clinit()方法执行过程中一些可能会影响程序运行行为的特点和细节。
<clinit()>
方法是由编译器自动收起类中所有类的变量的赋值动作和静态语句块(static{ }
)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量
,在前面的静态语句块可以赋值,但是不能访问,如下代码所示。
public class Test{
static{
//给变量赋值编译可以正常通过
i = 0;
//编译器会提示“非法向前引用变量”
System.out.print(i);
}
static int i = 0;
}
- clinit() 方法与类的构造函数不同,它不需要显示地调用父类构造器,虚拟机会保证子类的clinit方法执行之前,父类的clinit方法已经执行完毕。因此在虚拟机中第一个执行的clinit方法的类肯定是java.lang.Object。
- 由于父类的clinit方法先执行,也就意味着父类的静态语句块要比子类的静态语句块先执行。
- clinit方法对于类或者接口来说不是必须的,如果一个类没有静态语句块,也没有对变量进行赋值操作,那么编译器可以不为这个类生成clinit方法
- 虚拟机会保证一个类的clinit方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只有一个线程去执行这个类的clinit方法,其他线程都需要阻塞等待,知道活动线程执行clinit方法完毕。
类加载器ClassLoader