在Java语言里,编译时并不进行链接工作,类型的加载、链接和初始化工作都是在Java虚拟机执行过程中进行的。在Java程序启动时,Java虚拟机通过加载指定的类,然后调用该类的main方法而启动。在JVM启动过程中,外部class字节码文件会经过一系列的过程转化为JVM中执行的数据。这一系列的过程我们称为类加载过程。
从类被JVM加载到内存开始,到卸载出内存为止,整个生命周期包括:加载、链接、初始化、使用和卸载五个过程。其中链接又包括验证、准备和解析三个过程。
在整个生命周期中,加载、验证、准备、初始化和卸载五个过程的启动顺序是确定的,而解析过程在Java规范中并没有强制规定。这几个过程 启动 顺序确定,但是 执行 顺序并不是依次进行,其中有部分工作是交叉进行的,在后面的详情中会进行详细解释。
Java虚拟机规范并没有对何时进行类加载过程中的第一个步骤 加载 进行强制约束,那类加载的起点如何确定?Java虚拟机规范通过对 初始化 阶段进行严格规定,来保证 初始化 的完成,而作为其之前必须启动的过程,加载、验证、准备当然也需要在此之前开始。
Java虚拟机规定, 有且只有 以下五种情况时,必须立即对类进行初始化:
接口的初始化与类基本相同,唯一不同的是当一个接口初始化时,并不要求其父接口必须初始化,只有真正使用父接口时才会初始化。
下面详细介绍下类加载过程中加载、验证、准备、解析、初始化的具体动作。
在加载阶段,虚拟机需要完成以下3件事情:
对于Class对象,Java虚拟机规范并没有规定是存储在Java堆中,HotSpot虚拟机将其存放在方法区内。
验证作为链接过程中的第一步,大致会完成以下4个阶段的检验动作:
准备阶段正式为类的字段变量分配内存,并设置初始值。这些变量存储于方法区中,注意此处的变量为 类变量(被static修饰符修饰),而非实例变量。Java中数据类型的初始值见下表。
数据类型 | 初始值 |
---|---|
boolean | false |
byte | (byte) 0 |
char | /u0000 |
short | (short) 0 |
int | 0 |
long | 0L |
float | 0F |
double | 0D |
reference | null |
当类字段为常量类型时(即被static final修饰),由于字段的值已经确定,并不会在后面修改,此时会直接赋值为指定的值。如下面的变量value,将直接赋值为1。
public static final int value = 1; 复制代码
解析阶段将常量池中的符号引用替换为直接引用。在字节码文件中,类、接口、字段、方法等类型都是由一组符号来表示,其形式由Java虚拟机规范中的Class文件格式定义。在虚拟机执行特定指令之前,需要将符号引用转化为目标的指针、相对偏移量或者句柄,这样可以通过此类直接引用在内存中定位调用的具体位置。
在类的class文件中,包含两个特殊的方法:<cinit>和<init>。这两个方法由编译器自动生成,分别代表类构造器和构造函数。其中构造函数可以由变成人员实现,而类构造器则由编译器自动生成。而初始化阶段则负责调用类构造器,来初始化变量和资源。
<cinit>方法由编译器自动收集类的赋值动作和静态语句块(static{}块)中的语句合并生成的,它有以下特点:
public class Test { static { // 变量赋值编译可以正常通过 value = 2; // 访问变量编译失败 System.out.println(i); } static int value = 1; } 复制代码
正确掌握类加载的过程,可以对平常编程中的各种问题有更深入的了解。下面通过一个违背感觉的实例,来感受下掌握类加载过程的重要性。
class SingleTon { private static SingleTon singleTon = new SingleTon(); public static int count1; public static int count2 = 0; private SingleTon() { count1++; count2++; } public static SingleTon getInstance() { return singleTon; } } public class Test { public static void main(String[] args) { SingleTon singleTon = SingleTon.getInstance(); System.out.println("count1=" + singleTon.count1); System.out.println("count2=" + singleTon.count2); } } 复制代码
上面代码的执行结果为:
count1=1 count2=0 复制代码
按照初始化的触发条件,我们可知在main方法调用getInstance时,会进行SingleTon的类加载过程。
在准备阶段,会对变量添加初始值,此时singleTon=null,count1=0,count2=0。
在初始化阶段,先执行new SingleTon(),此时count1=count2=1,然后由于count1无赋值操作,所以count1=1。count2赋值为0,所以结果为count1=1,count2=0。
假如将**private static SingleTon singleTon = new SingleTon(); 语句移动到 public static int count2 = 0;**之后,则结果为count1=1,count2=1。