图示

image-20201212194346694

加载阶段

在加载类时,Java 虚拟机必须完成以下3件事情

  1. 通过类的全名,获取类的二进制数据流。
  2. 解析类的二进制数据流,在方法区中构建Java类模板对象
  3. 中创建java. lang. Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口。

注:

  • 类模板对象,是Java类在JVM内存中的一个快照,JVM将 从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,这样JVM在运行期便能通过类模板而获取Java类中的任意信息,能够对Java类的成 员变量进行遍历,也能进行Java方法的调用。反射的机制即基于这一基础。 如果JVM没 有将Java类的声明信息存储起来,则JVM在 运行期也无法反射。
  • 数组类本身不是由类加载器负责创建的,JVM在加载数组的时候加载的仅仅是数组的类型类(如String[] 加载String这个类型类),而数组的创建则由JVM在运行时根据需要直接创建的。如果是N维数组,类加载器会从最外层开始一层一层的递归加载,直到加载到非数组类型为止。

二进制流的获取方式

  • 通过文件系统读入一个class后缀的文件(最常见) 。

  • 读入jar、zip等归档数据包,提取类文件。

  • 事先存放在数据库中的类的二进制数据。

  • 使用类似于HTTP之类的协议通过网络进行加载。

  • 在运行时生成一段Class的二进制信息。

  • ……

链接阶段

Verification(验证)

目的是保证加载的字节码是合法、合理并符合规范的。

  1. 格式检查

    魔数检查(class文件前四个字节的16进制是不是0xCAFEBABE)。

    版本检查(高版本Java编译器编译出来的class文件无法在低版本运行)。

    长度检查(数据项的码,每一项长度是否正确)。

  2. 语义检查

    是否有类继承final(被定义为final的方法或者类无法被重写或继承)。

    是否有父类(在Java里,除了object外,其他类都应该有父类)。

    抽象方法是否有实现 (非抽象类是否实现了所有抽象方法或者接口方法)。

    是否存在不兼容的方法(比如方法的签名除了返回值不同,其他都一样, 这种方法会让虚拟机无从下手调度; abstract情况下的方法,就不能是final的了)。

  3. 字节码验证

    跳转指令是否指向正确位置(在字节码的执行过程中,是否会跳转到一条不存在的指令)。

    变量的赋值是不是给了正确的数据类型。

    函数的调用是否传递了正确类型的参数。

  4. 符号引用验证

    符号引用的直接引用是否存在(Class文件在其常量池会通过字符串记录自己将要使用的其他类或者方法。虚拟机会检查这些类或者方法确实是存在的,并且当前类有权限访问这些数据,如果一个需要使用类无法在系统中找到,则会抛出NoClassDefFoundError ,如果一个 方法无法被找到,则会抛出NoSuchMethodError)。

Preparation(准备)

虚拟机会为该类分配内存空间并为静态字段设置默认值

类型 默认初始值
byte (byte)0
short (short)0
int 0
long 0L
float 0.0f
double 0.0
char \u0000
boolean false
reference null

注:

  1. 这里不包含基本数据类型的字段用static final修饰的情况, 因为final在编译的时候就会分配了,准备阶段会显式赋值。
  2. 注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

Resolution(解析)

将类、接口、字段和方法符号引用转为直接引用。

符号引用就是一些字面量的引用,和虚拟机的内部数据结构和和内存布局无关。在Class类文件中,通过常量池进行了大量的符号引用。解析操作会把符号引用转为对应目标的直接引用,也就是得到类、字段、方法在内存中的指针或者偏移量。

初始化阶段

执行类的初始化方法:< clinit>();,该方法由Java编译器生成并由JVM调用,开发者无法自定义它,它是由类静态成员到的赋值语句及static代码块语句合并而产生的。即初始化阶段为类的静态变量赋予正确的初始值。

注:在加载一个类前,会先加载其父类,因此父类的clinit方法总在子类前被调用。

没有类初始化方法的情况

  • 一个类中并没有声明任何的类变量,也没有静态代码块时。
  • 一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时。
  • 一个类中只包含static final修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式

对于基本数据类型的字段来说,如果使用static final修饰,则显式赋值(直接赋值常量,而非调用方法)通常是在链接阶段的准备环节进行。对于String来说,如果使用字面量的方式赋值,使用static final修饰的话,则显式赋值通常是在链接阶段的准备环节进行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class InitializationTest {
public static int a = 1;//在初始化阶段< clinit>()中赋值
public static final int INT_CONSTANT = 10;//在链接阶段的准备环节赋值

public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100);//在初始化阶段< clinit>()中赋值
public static Integer INTEGER_CONSTANT2 = Integer.valueOf(1000);//在初始化阶段< clinit>()中赋值

public static final String s0 = "helloworld0";//在链接阶段的准备环节赋值
public static final String s1 = new String("helloworld1");//在初始化阶段< clinit>()中赋值

public static String s2 = "helloworld2";

public static final int NUM1 = new Random().nextInt(10);//在初始化阶段< clinit>()中赋值
}

< clinit>()的调用

类的主动使用:意味着会调用类的< clinit>()

  1. 当创建一个类的实例时,比如使用new关键字,或者通过反射(如Class.forName(“com.atguigu.java.Test”))、克隆、反序列化。
  2. 当调用类的静态方法时,即当使用了字节码invokestatic指令。
  3. 当使用类、接口的静态字段时(final修饰特殊考虑),比如,使用getstatic或者putstatic指令。(对应访问变量、赋值变量操作)
  4. 当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化(并不会先初始化它所实现的接口**,在初始化一个接口时,并不会先初始化它的父接口,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态字段时,才会导致该接口的初始化。)。
  5. 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化。
  6. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  7. 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。(涉及解析REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄对应的类)

类的被动使用:即不会进行类的初始化操作,即不会调用< clinit>()

  1. 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化。(当通过子类引用父类的静态变量,不会导致子类初始化)
  2. 通过数组定义类引用,不会触发此类的初始化。
1
Object[] objectList = new Object[10];//执行此句并不会使Object类的< clinit>()被调用
  1. 引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了。

  2. 调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

注:没有初始化的类,不意味着没有加载!

使用阶段

即可直接使用,调用它的静态字段、方法。使用new关键字创建实例对象。

卸载阶段

条件

  • 该类所有的实例都已经被回收。也就是Java堆中不存在该类及其任何派生子类的实例。
  • 加载该类的类加载器已经被回收。
  • 该类对应的java. lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
  • Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是被允许,而并不是和对象一一样,没有引用了就必然会回收。

END