浅谈Java类的加载,链接及初始化
类加载的过程
类的加载是指将类的.class文件中的二进制数据读入到内存中,将其转化为方法区中的运行时数据结构,在堆中生成一个代表这个类的java.lang.Class对象,并向Java虚拟机注册,以便于该类被引用时能够找到它。
类加载的过程分为三个步骤:加载、链接和初始化。
加载
加载阶段是将类装载至内存并生成由JVM管理的Class对象。Java虚拟机规范并没有明确规定从哪里加载类,但是大多数的虚拟机实现都是从文件系统中加载类二进制数据,当然也可以从ZIP、JAR文件和网络中获取。加载完成后,系统会为这个类生成一个java.lang.Class对象,作为访问方法区中这些数据结构的入口。
具体实现:
- 通过一个类的全限定名(全限定名包括包名和类名)获取其定义的二进制字节流。
- 将这个字节流所代表的静态储存结构转化为方法区的运行时数据结构。
- 在Java堆中生成一个代表这个类的java.lang.Class对象,作为访问方法区中这些数据结构的入口。
链接
链接阶段分为三个过程:验证、准备、解析
验证
验证阶段是保证被加载的类的正确性。验证可以分为文件格式验证、元数据验证、字节码验证、符号引用验证 4 个部分。
- 文件格式验证:保证展现数据的字节流符合 Class 文件格式的规范,并且能被当前运行的虚拟机理解。
- 元数据验证:对类的元数据信息进行语义分析的过程,以保证其符合 JVM 规范的要求。例如类访问修饰符的合法性,以及类中字段、方法描述符以及属性表等是否缺失。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。并确认程序中不会发生不被捕获的异常、变量的类型必须与指令能够产生的类型一致、跳转指令不能指向不良代码等。
- 符号引用验证:对类自身以外的信息进行匹配性检查,主要包括在类、字段、方法中使用的类描述符、字段描述符、方法描述符、方法句柄和调用点限定符的检查是否能够在当前程序执行环境下被解析。
准备
准备过程是为类的静态变量分配内存并设置默认值的过程。
具体实现:
每个字段都会有一个初始值。这里注意「字段初始值」和「默认值」是不同的。默认值是 Java 中变量声明时的初始值,而类变量(包括 static 变量和 static 块中的变量)的“默认值”是在“准备阶段”赋值的。
例如:
public class Test {
private static int a = 1;
private static final int b = 2;
}
以上代码对应的字节码文件,执行准备阶段时:
public class Test {
private static int a; // 这里也可以设置为 0
private static final int b = 2;
}
这里最终 a 值不是 1,而是 0。因为它是一个类变量,在类加载的过程中,它的正确初始值是 0。
解析
解析阶段是将类中的符号引用转化为直接引用的过程。符号引用就是指向某个目标的符号,可以是类名、方法名、属性名。而直接引用就是指向目标的指针、相对偏移量或者句柄。将符号引用转化为直接引用可以理解为程序编译后就已经存在,需要在类加载阶段就完成映射。
初始化
初始化阶段是对类进行初始化,主要包括为类的静态变量赋值。
通俗的讲,只有在准备阶段中,类变量被赋予了系统推荐的初始值后,才会进入类的初始化阶段。类初始化阶段才真正开始执行类中定义的 Java 代码。
具体实现:
- 如果该类存在表示类初始化方法的类构造器
(),则执行该构造器。类构造器是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。 - 类的初始化是同步进行的。随着线程的增加,也申请需要触发初始化的类的 Class 对象的锁,所以类初始化是线程安全的。
- 一个类被初始化后,就会进入“初始化完毕状态”,即不需要再次初始化。
示例说明
示例 1:加载与初始化顺序
public class LoadClass {
static {
System.out.println("这是静态代码块.");
}
public static void main(String[] args) {
System.out.println("这是main方法.");
System.out.println(A.name); // 输出 A
}
}
public class A {
public static String name = "A";
static {
System.out.println("这是A的静态代码块.");
}
}
以上程序中,先有 LoadClass,它引入了 A,而 A 中定义了一个 static 的“变量 name”和一个静态代码块。因此,对于这段代码的执行顺序是:先执行 LoadClass 的静态代码块,再执行main方法,最后输出字符串 A。
示例 2:验证阶段
当 Java 类的字节码被加载到 Java 虚拟机中时,加载器会检查其字节码文件的格式是否正确,以及其他的二进制数据的正确性。如果字节码文件有问题,Java 虚拟机就会抛出 ClassFormatError 异常。
当 Java 类通过字节码文件验证阶段后,Java 虚拟机必须尽可能验证该类的元数据和字节码语义是否正确。如果 Java 类的字节码文件有使用 Java 虚拟机规范所禁止的操作、或者使用了不正确的数据,Java 虚拟机就会抛出 VerifyError 异常。下面是一个验证阶段的示例:
public class ValidateClassFileSample {
public static void main(String[] args) {
int x = 1;
int y = 1;
int z = x + y;
System.out.println(z);
}
}
编译以上代码后,删除 ValidateClassFileSample 中的 .class 文件,执行命令行 java ValidateClassFileSample 会抛出以下异常信息:
Exception in thread "main" java.lang.VerifyError: (class: ValidateClassFileSample, method: main signature: ([Ljava/lang/String;)V) Illegal target of jump or loop
这是因为编译器可能生成了同样的变量表、局部表和代码,但是它们可能引用不同的标签。一些字节码语句与标签不能对应,因此导致了 VerifyError。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:浅谈Java类的加载,链接及初始化 - Python技术站