1. 什么是JVM的类加载?
虚拟机将class文件加载到内存,并对数据校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。

类的加载一般分为五个阶段:加载,校验,准备,解析,初始化。而校验,准备,解析被统称为连接阶段。
加载:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将获取到的二进制字节流转化成一种数据结构并放进方法区
- 在内存中生成一个代表此类的java.lang.Class对象,作为访问方法区中各种数据的接口
简单来说:
在加载阶段,JVM通过类的名称来找到需要加载类的二进制class文件,然后生成一个此类的class对象。
校验:
目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
准备:
在准备阶段,主要的任务是给类变量赋初始值,也就是零值,类变量就是static修饰的变量。这里不包含用final修饰的类变量,因为final在编译的时候就会分配了。注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
解析:
解析阶段主要是将常量池内的符号引用替换为直接引用的过程。符号引用是用一组符号来描述目标,可以是任何字面量,而直接引用则是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。通常而言一个符号引用在不同虚拟机实例翻译出来的直接引用一般不会相同。
和C之类的纯编译型语言不同,Java类文件在编译过程中只会生成class文件,并不会进行连接操作,这意味在编译阶段Java类并不知道引用类的实际地址,因此只能用“符号引用”来代表引用类。举个例子来说明,在com.sbbic.Person类中引用了com.sbbic.Animal类,在编译阶段,Person类并不知道Animal的实际内存地址,因此只能用com.sbbic.Animal来代表Animal真实的内存地址。在解析阶段,JVM可以通过解析该符号引用,来确定com.sbbic.Animal类的真实内存地址(如果该类未被加载过,则先加载)。
主要有以下四种:
- 类或接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
初始化:
到了初始化阶段,才开始真正执行用户编写的java代码了。初始化是为类的静态变量赋予正确的初始值。在准备阶段,变量都被赋予了初始值,但是到了初始化阶段,所有变量还要按照用户编写的代码重新初始化。换一个角度,初始化阶段是执行类构造器<clinit>()方法的过程。
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static语句块)中的语句合并生成的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问。
<clinit>()方法与类的构造函数<init>()方法不同,它不需要显示地调用父类构造器,虚拟机会在子类的<clinit>()方法执行之前,父类的<clinit>()已经执行完毕,因此在虚拟机中第一个被执行的<clinit>()一定是java.lang.Object的。
父类中的静态语句块优于子类的变量赋值操作
static class Parent {
public static int A=1;
static {
A=2;
}
}
static class Sub extends Parent{
public static int B=A;
}
public static void main(String[] args) {
System.out.println(Sub.B);
}
这里的步骤为:
首先加载子类sub,类变量B先赋初始值null,然后开始加载父类,执行父类的类加载,A先等于1,然后在静态代码块中,A又赋值为2,所以最后的B被赋值为2.
虚拟机会保证一个类的<clinit>()方法在多线程环境中能被正确的枷锁、同步。如果多个线程初始化一个类,那么只有一个线程会去执行<clinit>()方法,其它线程都需要等待。
本文详细介绍了Java虚拟机JVM的类加载过程,包括加载、校验、准备、解析和初始化五个阶段。每个阶段都有其特定的任务,如加载阶段负责将class文件转化为内存中的数据结构,初始化阶段执行类构造器<clinit>()方法。在解析阶段,符号引用被替换为直接引用,确保多线程环境下的正确同步。整个过程确保了Java类型的正确性和安全性。
9703

被折叠的 条评论
为什么被折叠?



