一、加载

1. 完成三件事

image.png

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

2. 获取二进制字节流的方式

  • ZIP压缩包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础。
  • 网络中获取,这种场景最典型的应用就是Web Applet。
  • 运行时计算生成,这种场景使用得最多的就是动态代理技术,在java.lang.reflect.Proxy中,就是用 了ProxyGenerator.generateProxyClass()来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流。
  • 其他文件生成,典型场景是JSP应用,由JSP文件生成对应的Class文件。
  • 数据库中读取,这种场景相对少见些,例如有些中间件服务器(如SAP Netweaver)可以选择 把程序安装到数据库中来完成程序代码在集群间的分发。
  • 可以从加密文件中获取,这是典型的防Class文件被反编译的保护措施,通过加载时解密Class文 件来保障程序运行逻辑不被窥探。

3. 数组类的加载

数组本身并不是由类加载器创建的,而是由JVM在运行时根据需求而直接创建的,但数组的类型仍然需要类加载器创建
创建数组的过程:
引用类型数组:

  1. 遵循定义的加载过程递归加载和创建数组的元素类型
  2. JVM使用指定的元素类型和数组的维度来创建数组类

基本数据类型数组:
JVM直接根据数组的维度创建数组

注意:如果数组的元素类型是引用类型,那么数组的访问性由数组的元素类型的访问性决定。如果是基本数据类型,数组的访问性被缺省定义为public

二、验证

1. 文件格式验证

第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。这一阶
段可能包括下面这些验证点:

  • 是否以魔数0xCAFEBABE开头。
  • 主、次版本号是否在当前Java虚拟机接受范围之内。
  • 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
  • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
  • CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据。
  • Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。

实际上第一阶段的验证点还远不止这些,该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。这阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证之后,这段字节流才被允许进入Java虚拟机内存的方法区中进行存储,所以后面的三个验证阶段全部是基于方法区的存储结构上进行的,不会再直接读取、操作字节流了。

2. 元数据验证

第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要
求,这个阶段可能包括的验证点如下:

  • ·这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)。
  • ·这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
  • ·如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
  • ·类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方
  • 法重载,例如方法参数都一致,但返回值类型却不同等)。
  • ……

第二阶段的主要目的是对类的元数据信息进行语义校验,保证不存在与《Java语言规范》定义相
悖的元数据信息。

3. 字节码验证

第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定 程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型校验完毕以后,这阶段就要 对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为,例如:

  • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于“在操作
  • 栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中”这样的情况。
  • 保证任何跳转指令都不会跳转到方法体以外的字节码指令上。
  • 保证方法体中的类型转换总是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全 的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的。

4. 符号引用验证

  • 符号引用中通过字符串描述的全限定名是否能找到对应的类。
  • 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。
  • 符号引用中的类、字段、方法的可访问性(private、protected、public、)是否可被当 前类访问。

三、准备

简言之,为类的静态变量分配内存,并将其初始化为默认值
注意:

  • 这里不包含基本数据类型的字段用static final修饰的情况,因为final在编译的时候就会分配了,准备阶段会显示赋值
  • 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到java堆中
  • 在这个阶段并不会像初始化阶段中那样会有初始化或者代码被执行

四、 解析

简言之,将类、接口、字段和方法的符号引用转为直接引用

1. 类或接口的解析

2. 字段解析

3. 方法解析

4. 接口方法解析

五、初始化

简言之,为类的静态变量显示赋值
到了初始化阶段,才真正开始执行类中定义的Java程序代码

1. 方法

  • 该方法由java编译器生成,并由JVM调用。开发者无法自定义,更无法在java程序中调用该方法
  • 它是由类静态成员的赋值语句以及static语句块合并产生的
  • 加载父类的方法
  • 方法不一定会产生,如下情况不会生成方法:
    • 一个类中并没有生明任何类变量,也没有静态代码块。

例:public int num = 1;

  • 一个类中声明类变量,但是没有明确使用类变量的初始化以及静态代码块来执行初始化操作

例:public static int num1;

  • 一个类中包含static final修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式

例:public static final int num2 = 1;

2. 赋值时机

public static final int i = 1;
public static final String str = "hello";
static final修饰的字段,等号右边是确定的值,那么就是在准备阶段赋值。其余情况在初始化阶段赋值

3. 方法线程安全性

因为方法是线程安全的,因此,如果一个类的方法加载时间过长,就可能造成多个线程阻塞,引发死锁。并且这种死锁是很难发现的,因为看起来它们并没有可用的锁信息

4. 类的初始化情况:主动使用vs被动使用

Ⅰ 主动使用

主动使用必须要对类进行初始化操作

以 下情况会主动使用:

  1. 创建一个类的实例
  2. 调用类的静态方法
  3. 使用类、接口的静态字段(final修饰符特殊考虑
  4. 使用Java.lang.reflect包中的方法反射类的方法
  5. 当初始化子类时,如果发现其父类还没初始化,则需要先触发其父类的初始化。这条规则并不适用于接口。
  6. 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化
  7. 当虚拟机启动时,用户需要指定一个要执行的主类(包含mian()方法的那个类),虚拟机会先初始化这个类
  8. 当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类


    补充说明
    针对第五条(注意第六条的特殊情况):

  • 初始化类,并不会初始化它所实现的接口
  • 初始化子接口,也不会初始化父类接口

Ⅱ 被动使用

除了主动使用的情况就是被动使用