【目录】 【上一篇:JVM 与 Java 体系结构】 【下一篇:类加载器】
一、类的加载过程
类加载子系统(ClassLoader)负责从文件系统或网络中加载 class 文件,至于 class 文件的运行,则由执行引擎(Execution Engine)执行;加载的类信息存放于内存中的方法区(Metaspace space)。
类加载子系统在加载时分为三个阶段: 加载阶段、链接阶段、初始化阶段。
1、加载过程详解
1.1、Loading(加载)阶段
1.1.1、加载完成的操作
加载的理解:
所谓加载,简而言之就是 将 Java 类的字节码文件加载到机器内存中,并在内存中构建出 Java 类的原型 —— 类模板对象;这个类模板对象包含了字节码文件中的常量池、类字段、类方法等各种信息。这样 JVM 在运行期就能获取到该类的任意信息,实现对 Java 类的各种操作。
💡 加载阶段与链接阶段的部分动作是交叉进行的,加载阶段尚未完成,链接阶段可能已经开始,但它们之间仍然保持着固定的先后开始顺序。
加载完成的操作:
加载阶段,就是通过类的全限定名,找到类的二进制数据,生成 Class 的实例。
在加载类时,Java 虚拟机必须完成以下 3 件事:
- 通过类的全限定名,获取类的二进制数据流;
- 解析类的二进制数据流,并在方法区内部生成一个类数据结构(Java 类模型);
- 在堆内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据访问入口。
1.1.2、二进制流的获取方式
对于类的二进制数据流,虚拟机可以通过多种途径产生或获取(只要读取的字节码符合 JVM 规范即可):
- 虚拟机可以通过文件系统读取一个 class 文件;
- 读取 jar、zip 等归档数据包,提取类文件;
- 事先存放在数据库中的二进制数据;
- 从网络中进行加载;
- 在运行时生成一段 Class 的二进制数据。
1.1.3、类模型与 Class 实例的位置
类模板位置:
- 加载的类在 JVM 中创建相应的类结构,类结构会存储在方法区(JDK8 之前,永久代;JDK8 及之后,元空间);
Class 实例的位置:
- 堆。
1.1.4、数组类加载
创建数组类的情况稍微有些特殊,因为 数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要而直接创建 的,但数据的元素仍然需要依靠类加载器去创建。
- 如果数组的组件类型是引用类型,那就采用加载过程去加载这个类型,数组将被标识在加载该组件类型的类加载器的类名称空间上;(这点很重要,因为它是判断数组中元素是否相等的重要条件)
- 如果数组的组件类型是非引用类型(int[ ]),JVM 会把数组标记为与引导类加载器相关联。
1.2、Linking(链接)阶段
当类的二进制流数据加载到方法区之后,就开始链接操作,链接阶段又分为:验证、准备、解析三个小阶段。
1.2.1、验证阶段
- 确保二进制数据是合法、合理并符合规范的。
- 主要包括四种验证方式:文件格式验证、元数据验证、字节码验证、符号引用验证;
- 其中 格式验证会和加载阶段一起执行。验证通过之后,类加载器才会成功将类的二进制数据信息加载到方法区中;
- 格式验证之外的验证操作会在方法区中进行。
格式验证:
- 是否以魔数
0xCAFEBABE
开头; - 主版本和副版本号是否在当前 Java 虚拟机的支持范围内;
- 数据中每一个项是否都有正确的长度等;
- 常量池中的常量是否有不被支持的常量类型等。
语义检查:
- 是否所有的类都有父类存在(在 Java 中,除了 Object 之外,其他类都应该有父类);
- 被 final 修饰的方法或类是否被重写或继承;
- 非抽象类是否实现了所有抽象方法或接口方法;
- 是否存在不兼容的方法等(错误的方法重载)。
字节码验证:
- 在字节码执行过程中,是否会跳转到一条不存在的指令;
- 函数的调用是否传递了正确的参数;
- 变量的赋值是否给了正确的数据类型等。
栈映射帧(StackMapTable)就是在这个阶段,用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型。
符合引用验证:
- Class 文件在其常量池会通过字符串记录自己将要使用的其他类或则方法。因此,在验证阶段,虚拟机会检查这些类或者方法确实是存在的,并且当前类有权限访问这些数据。比如一个需要使用的类无法在系统中找到,则会抛出 NoClassDefFoundError ,一个方法无法被找到,则会抛出 NoSuchMethodError。(
此阶段会在解析环节才执行
)
1.2.2、准备阶段
在准备阶段,虚拟机就会为这个类分配相应的内存空间,为类的静态变量分配内存,并将其初始化为默认值。
💡 注意:
- 这里不包含基本数据类型的字段用 static final 修饰的情况,被 final 修饰的变量,在编译期间就已经分配好了,在准备阶段是显示的赋值。
- 这里不会为实例变量分配初始值,类变量是分配在方法区中,实例变量是会随具体的对象一起分配到 Java 堆中;
- 这个阶段并不会像初始化阶段中那样有初始化代码执行。
Java 并不支持 boolean 类型,对于 boolean 类型,其内部实现是 int,由于 int 的默认值是 0,所以 boolean 的默认值就是 false;
1.2.3、解析阶段
- 将类、接口、字段和方法的 符号引用 转化为 直接引用;
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info,CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。
💡 符号引用:符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机内存布局无关,引用的目标不一定加载到内存中。在 Java 中,编译时,java 类并不知道所引用类的实际地址,因此只能用符号引用来代替。
💡直接引用:直接引用可以是直接指向目标的指针(地址)。直接引用是和虚拟机布局相关的,同一个符号引用在不同的虚拟机实际上翻译出来的直接引用一般不会相同。如果有了直接引用,那么引用的目标必定已经被加载到内存中,明确分配了内存。
1.3、Initialization(初始化)阶段
类的初始化是类装载的最后一个阶段,此时类才会开始执行 Java 字节码。初始化阶段的重要工作是执行类的初始化方法:<clinit>() 方法,为所有类变量赋予正确的初始值。
- 该方法仅能由 Java 编译器自动生成并由 JVM 调用,开发人员无法自定义一个同名的方法,也无法直接在 Java 代码中调用;
- 是 java 编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来(类静态成员的赋值语句以及 static 语句块合并产生的。(如果没有静态变量或则静态语句块、或者说有静态变量,没有显示赋值,那么该方法就不会生成))
- <clinit>() 不同于类构造器(类构造器是 <init>()),若该类有父类,会优先保证父类的构造器先执行完毕,在执行子类的构造器;
- <clinit>() 方法是线程安全的(虚拟机必须保证一个类的构造方法在多线程下被同步加锁。)
💡 问:使用 static final 修饰的字段,其显示赋值操作,到底是在哪个阶段进行的?
情况1:在链接阶段的准备环节赋值; 情况2:在初始化阶段的 <clinit>() 中赋值;
解析:public static final int INT_CONSTANT = 10; // 在链接阶段的准备环节赋值
public static final int NUM1 = new Random().nextInt(10); // 在初始化阶段 <clinit>() 中赋值
public static int a = 1; // 在初始化阶段 <clinit>() 中赋值public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100); // 在初始化阶段 <clinit>() 中赋值
public static Integer INTEGER_CONSTANT2 = Integer.valueOf(100); // 在初始化阶段 <clinit>() 中概值public static final String s0 = “helloworld0”; // 在链接阶段的准备环节赋值
public static final String s1 = new String(“helloworld1”); // 在初始化阶段 <clinit>() 中赋值
public static String s2 = “hellowrold2”; // 在初始化阶段 <clinit>() 中赋值最终结论:
被 static final 修饰的字段,其值类型为基本数据类型或 “” 形式声明的字符串常量,在链接阶段的准备环节赋值,否则在初始化阶段 <clinit>() 中赋值(涉及到方法或构造器调用)。
1.4、Using(使用)阶段
实现具体的业务逻辑
1.5、Unloading(卸载)阶段
一个类何时结束生命周期,取决于代表它的 Class 对象何时结束生命周期。
- 该类所有的实例都已经被回收;
- 加载该类的类加载器已经被回收;
- 该类对应的 java.lang.Class 对象没有在任何地方引用。