Loading阶段

所谓加载,就是jvm将字节码文件通过二进制数据流加载进机械内存,并在内存(方法区)中构建一个java内存模型存放起来,这个内存模型包含了这个类的所有信息(属性、方法……),不严格的来说,可以理解为我们反射中所说的Class类对象(存放在堆中,可作为访问这个类信息的入口)。而这个内存模型,就是java反射机制的源头,如果虚拟机不存储类的信息(即内存模型),那么java就不会存在反射。

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

  • 通过一个类的全限定名来获取其定义的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口

类加载过程 - 图1

Linking阶段

验证(Verification)

检测字节码文件中的字节码是否符号jvm要求的语法规范,大致有以下过程:
类加载过程 - 图2

准备(Preparation)

当类经过验证之后,准备阶段是为类的静态变量分配内存,并将其初始化为默认值的过程。这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。零值表如下:
类加载过程 - 图3
在准备阶段有几个值得注意的点:

  • 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
  • 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
  • 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
  • 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。

    注意:

    1. 静态常量static final不在改阶段赋值,它早早地在编译阶段就进行了赋值操作;
    2. 该阶段不会按照程序员的意愿执行代码,比如一个类的静态变量:“static int a=666;”经过该阶段,a的值是默认值0,而不是我们写的赋值代码中的666;

解析(Resolution)

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

解释:

  • 符号引用:一组符号来描述目标,可以是任何字面量,只要使用时能够无歧义的定位到目标即可,使用符号引用时,被引用的目标不一定已经加载到内存中。
  • 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。使用直接引用时,引用的目标必定已经存在于虚拟机的内存中了。

如果使用符号引用,虚拟机其实也不知道具体引用的对象的内存地址,那么也就无法真正的调用到该对象,所以要把符号引用转为直接引用,这样就能够真正定位到对象在内存中的地址,如果在解析阶段符号引用转直接引用失败,就说明类还没有被加载到内存中,就会报错。

initialization阶段

初始化阶段是为静态变量赋予正确的初始值并执行类的静态代码块,jvm会按照顺序自上而下运行类中的变量赋值语句和静态语句,如果有父类,则首先按照顺序运行父类中的变量赋值语句和静态语句,所以该阶段才开始执行字节码,也就是这时候才真正执行程序员按照自己意愿编写的代码。下面是JVM的初始化步骤:

  1. 假如这个类还没有被加载和连接,则程序先加载并连接该类
  2. 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  3. 假如类中有初始化语句,则系统依次执行这些初始化语句

在该阶段所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是方法,即类/接口初始化方法。任何invoke之类的字节码都无法调用方法,因为该方法只能在类加载的过程中由JVM调用。如果父类还没有被初始化,那么优先对父类初始化,但在方法内部不会显示调用父类的方法,由JVM负责保证一个类的方法执行之前,它的父类方法已经被执行。JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。

初始化阶段为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

  1. 声明类变量是指定初始值
  2. 使用静态代码块为类变量指定初始值

类初始化时机是当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

  1. 创建类的实例,也就是new的方式
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
  4. 反射(如Class.forName(“com.shengsiyuan.Test”))
  5. 初始化某个类的子类,则其父类也会被初始化
  6. Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类