类加载过程一共分三个阶段,第一个阶段是加载,然后是链接,最后是初始化,如下图
一、加载
加载是类加载的第一个过程,在加载阶段,Java虚拟机需要完成以下三件事:
(1)通过一个类的全限定名来获取定义此类的二进制字节流
(2)通过这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
二、链接
链接阶段又要分三个小阶段,分别是验证、准备和解析。
1.验证
验证是链接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身安全,主要包括四种验证:文件格式验证、元数据验证、字节码验证和符号引用验证。下面让我们看一下字节码文件
public class Test {
private static int a = 1;
public Test() {
}
public static void main(String[] args) {
System.out.println(a);
}
}
上面的图片是使用winHex打开的字节码文件,所有的Class文件格式都是以0xCAFEBABE开头的
2.准备
准备阶段主要做两件事,一是为类变量(static修饰的变量)分配内存,二是为类变量设置默认值
注意:
(1)这里的分配内存是在方法区
(2)默认值通常情况是数据类型的零值
(3)final修饰的static在准备阶段会被显示初始化
(4)这阶段实例变量不会分配内存和初始化
3.解析
将常量池内符合引用转换为直接引用的过程。解析操作往往会伴随着JVM在执行完初始化再执行。符合引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中,直接引用就是直接指向目标的指针、相对偏移量或一个间接定位目标的句柄。通俗的讲,在加载一个类时,会设计到其他类,这些类不能全部加载到字节码文件中,通过符号引用引用相关的类型
public class Test {
private static int a = 1;
public static void main(String[] args) {
System.out.println(a);
}
}
通过javap -v Test.class对文件反编译后
#29 = Utf8 com/hzq/chapter/Test
#30 = Utf8 java/lang/Object
#31 = Utf8 java/lang/System
#32 = Utf8 out
#33 = Utf8 Ljava/io/PrintStream;
#34 = Utf8 java/io/PrintStream
#35 = Utf8 println
#36 = Utf8 (I)V在常量池会看到上面的内容,会发现会加载很多结构,例如Object,在解析阶段会直接将符号引用转换为直接引用
三、初始化
初始化阶段就是执行类构造器方法<clinit>()的过程。
(1)<clinit>方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而产生的,收集的顺序和语句在源文件出现的顺序一样
public class Test {
private static int a = 1;
public static void main(String[] args) {
System.out.println(a);
}
}
上面代码在编译后,使用idea插件jclasslib打开(view -> show Bytecode With Jclasslib)
读者可能会问,万一代码中没有静态变量或者静态代码块,那么会有clinit方法产生吗?答案是没有的,你可以试试把上面的static去掉,会得到下面的结果
<clinit>()与类的构造器函数不同,它不需要显示地调用父类构造器,Java虚拟机会保证在先父后子执行<clinit>方法。
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会执行生成<clinit()>方法。但与类不同的是,它不满足先父后子执行<clinit()>方法,只有当父接口变量被使用时才被初始化,并且接口的实现类在初始化也不会执行接口的<clinit>()方法
Java虚拟机会保证一个类的<clinit>方法在多线程只被执行一次,若出现多个线程同时初始化一个类,则会发生阻塞等待。
在上面的图片中,读者可能会注意到<init>和<clinit>,每个类都至少有一个构造器,那么<init>对应类中中的构造器。 例如如下代码,在调用Test构造器之前会默认先调用Object的构造器方法
public class Test {
private int num ;
public Test(int num){
this.num = num;
}
public static void main(String[] args) {
}
}