JAVA虚拟机-类加载机制

时间:2022-12-21 09:47:13

JAVA虚拟机–类加载机制(一)

类加载机制:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接是的Java类型,这就是虚拟机的类加载机制。

一、类加载的时机

类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接。

JAVA虚拟机-类加载机制

那什么时候需要开始类加载过程的第一个阶段:加载?Java虚拟机没有进行强制的约束。但是对于初始化阶段,虚拟机规范了有且只有5中情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前):

  1. 遇到new、getstatic、putstatic或者invokestatic这4条字节码指令时,如果类没有进行过初始化,则必须先触发初始化。
  2. 对类进行反射调用的时候,如果类没有进行过初始化,则必须先触发初始化。
  3. 当初始化一个类的时候,如果发现其父类还没进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个需要执行的主类(包含main()方法的那个类),虚拟机会对其进行初始化。
  5. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic等方法句柄,并且这个方法所对应的类没进行初始化,则必须先初始化。

二、类加载的过程

1、加载

在加载阶段,虚拟机需要完成以下3件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口。

不过,对于数组类而言(MyClass[] mcs = new MyClass[5];),情况有所不同,数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。但数组类的元素类型(数组去掉所有维度的类型)还是需要靠类加载器去创建。

2、验证

验证是连接阶段的第一步,这个阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

验证阶段大致上会完成下面4个阶段的检验动作:

文件格式验证,元数据验证、字节码验证、符号引用验证。

验证阶段是非常重要的,这个阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的攻击。我这个只是稍微介绍下验证阶段,有兴趣的读写可自行深入了解。

3、准备

准备阶段是正式为类变量分配内存的并设置类变量初始值的阶段,这个变量所使用的内存都将在方法区中进行分配。首先需要注意的是,这个时候进行内存分配的只包括类变量(被static修饰的变量),而不包括实例变量,实例变量是在对象实例化时随着对象一起分配在Java堆中。其次,这里的说的初始值是指数据类型的零值,假设一个类变量定义为:

public static int value = 123;

那变量在准备阶段过后的初始值是0而不是123,因为这个时候尚未开始执行任何Java方法,把value赋值为123的putstatic指令是程序被编译后。下面给出一些基本数据类型的零值:

JAVA虚拟机-类加载机制

但也有一些特殊情况,比如上面类变量value的定义改为:

public static final int value = 123;

那编译时Javac将会为value生产ConstantValue属性(作用是通知虚拟机自动为静态变量赋值),在准备阶段虚拟机就会将value赋值为123。

4、解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。

直接引用:直接引用可以是直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。

5、初始化

类初始化是类加载过程中的最后一步,这个阶段,才真正开始执行类中定义的Java程序代码。

在准备阶段,变量已经赋值过一个次系统要求的初始值,而在初始化阶段,则根据程序制定的主观计划去初始化类的变量和其他资源,或者从一个角度类表达:初始化阶段是执行类构造器 <clinit >()方法的过程。

<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句结合并产生的,编译器收集顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但不能访问,如下代码中所示:

public class Test{
static{
i=0; //给变量赋值可以正常编译通过
System.out.print(i); //这句编译器会提示“非法向前引用”
}
static int i = 1;
}

<clinit>()方法与类的构造函数不同,它不需要显示的调用父类的构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕,所以,在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object.

由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,所以下面代码中字段B的值将会是2而不是1。

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);
}

<clinit>()方法并不是必需的,如果类中没有静态语句块,也没有对变量的赋值操作,就不为这个类生成<clinit>()方法。

接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。不同的是,执行接口的<clinit>()方法不需要先执行父类接口的<clinit>()方法。只有当父接口中的变量被使用时,父接口才会初始化。

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步。

上面主要对类加载的各个阶段进行了简要的介绍,相信读者已经对类加载有了初步的了解。

作者:陈伟杰:原文地址