在 Java 中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚 拟机预先定义,引用数据类型则需要进行类的加载。
按照 Java 虚拟机规范,从 Class 文件到加载到内存中的类,到类卸载出内 存位置,它的整个生命周期包括如下七个阶段:
验证、准备、解析 3 个部分统称为链接(Linking)
程序中类的使用过程:
课间休息会哈
加载阶段,简言之,查找并加载类的二进制数据,生成 Class 的实例
加载时,Java 虚拟机必须完成以下 3 件事情:
对于类的二进制数据流,虚拟机可以通过多种途径产生或获得。(只要所读取 4 的字节码符合 JVM 规范即可)
在获取到类的二进制信息后,Java 虚拟机就会处理这些数据,并最终转为一 个 java.lang.Class 的实例
如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError这句话的意思是 如果 输入数据 不符合JVM规范就会抛出异常。
类模型的位置
加载的类在 JVM 中创建相应的类结构,类结构会存储在方法区(JDK 1.8 之 前:永久代;JDK 1.8 之后:元空间)
Class 实例的位置
类将 .class 文件加载至元空间后,会在堆中创建一个 java.lang.Class 对象, 用来封装类位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建 的,每个类都对应有一个 Class 类型的对象
外部可以通过访问代表 Order 类的 Class 对象来获取 Order 的类数据结构
Class 类的构造方法是私有的,只有 JVM 能够创建 java.lang.Class 实例是访问类型元数据的接口,也是实现反射的关键数据、入口。 通过 Class 类提供的接口,可以获得目标类所关联的 .class 文件中具体的数据 结构:方法、字段等信息。
当类加载到系统后,就开始链接操作,验证是链接操作的第一步
它的目的是保证加载的字节码是合法、合理并符合规范的
验证的步骤比较复杂,实际要验证的项目也很繁多,
大体上 Java 虚拟机需 要做以下检查,如图所示:
魔数解释:魔法值(即魔数)指的是未经预先定义的常量
想要了解这个冷知识的话:传送门
准备阶段(Preparation),简言之,为类的静态变量分配内存,并将其初始化为 默认值。
当一个类验证通过时,虚拟机就会进入准备阶段。
在这个阶段,虚拟机就会 为这个类分配相应的内存空间,并设置默认初始值。
Java 虚拟机为各类型变量 默认的初始值如表所示:
注意1:
Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int, 由于 int 的默认值是 0,故对应的,boolean 的默认值就是 false
注意2:
拓展:如果使用字面量的方式定义一个字符串的常量的话,也是在准备环节 直接进行显式赋值
在准备阶段(Resolution),简言之,将类、接口、字段和方法的符号引用转为 直接引用
符号引用就是一些字面量的引用,和虚拟机的内部数据结构和内存分布无关。
比较容理解的就是在 Class 类文件中,通过常量池进行了大量的符号引用。
但是在程序实际运行时,只有符号引用是不够的,比如当如下 println() 方法被调 用时,系统需要明确知道该方法的位置。
举例:
输出操作 System.out.println() 对应的字节码:
invokevirtual #24<java/io/PrintStream.println>
以方法为例,Java 虚拟机为每个类都准备了一张方法表,将其所有的方法都 列在表中,当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏 移量就可以直接调用该方法。通过解析操作,符号引用就可以转变为目标方法在 类中方法表中的位置,从而使得方法被成功调用
所谓解析就是将符号引用转为直接引用,也就是得到类、字段、方法在内存 中的指针或者偏移量。因此,可以说,如果直接引用存在,那么可以肯定系统中 存在该类、方法或者字段。
初始化阶段,简言之,为类的静态变量赋予正确的初始值。
类的初始化是类装载的最后一个阶段。如果前面的步骤都没有问题,那么表 示类可以顺利装载到系统中。此时,类才会开始执行 Java 字节码。(即:到了初 始化阶段,才真正开始执行类中定义的 Java 程序代码) 初始化阶段的重要工作是执行类的初始化方法:() 方法
该方法仅能由 Java 编译器生成并由 JVM 调用,程序开发者无法自定义一 个同名的方法,更无法直接在 Java 程序中调用该方法,虽然该方法也是由 字节码指令所组成
它是类静态成员的赋值语句以及 static 语句块合并产生的
说明
- /**
- *
- * 哪些场景下,Java 编译器就不会生成<clinit>()方法
- */
- public class InitializationTest1 {
- //场景 1:对应非静态的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法
- public int num = 1;
- //场景 2:静态的字段,没有显式的赋值,不会生成<clinit>()方法
- public static int num1;
- //场景 3:比如对于声明为 static final 的基本数据类型的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法
- public static final int num2 = 1;
- }
-
- /**
- *
- * 说明:使用 static + final 修饰的字段的显式赋值的操作,到底是在哪个阶段
- 进行的赋值?
- * 情况 1:在链接阶段的准备环节赋值
- * 情况 2:在初始化阶段<clinit>()中赋值
- *
- * 结论:
- * 在链接阶段的准备环节赋值的情况:
- * 1. 对于基本数据类型的字段来说,如果使用 static final 修饰,则显式赋值(直
- 接赋值常量,而非调用方法)通常是在链接阶段的准备环节进行
- * 2. 对于 String 来说,如果使用字面量的方式赋值,使用 static final 修饰的
- 话,则显式赋值通常是在链接阶段的准备环节进行
- *
- * 在初始化阶段<clinit>()中赋值的情况
- * 排除上述的在准备环节赋值的情况之外的情况
- *
- * 最终结论:使用 static + final 修饰,且显示赋值中不涉及到方法或构造器调
- 用的基本数据类型或 String 类型的显式赋值,是在链接阶段的准备环节进行
- */
- public class InitializationTest2 {
- public static int a = 1; //在初始化阶段<clinit>()中赋值15
- 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>()中赋值
- }
-
-
Java 程序对类的使用分为两种:主动使用 和 被动使用
Class 只有在必须要首次使用的时候才会被装载,Java 虚拟机不会无条件地 装载 Class 类型。Java 虚拟机规定,一个类或接口在初次使用前,必须要进行 初始化。这里指的"使用",是指主动使用,主动使用只有下列几种情况:(即: 如果出现如下的情况,则会对类进行初始化操作。而初始化操作之前的加载、验 证、准备已经完成)
(涉及解析 REF_getStatic、REF_putStatic、REF_invokeStatic 方法 17 句柄对应的类)
针对 5,补充说明:当 Java 虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口
除了以上的情况属于主动使用,其他的情况均属于被动使用。
被动使用不会 引起类的初始化 也就是说:并不是在代码中出现的类,就一定会被加载或者初始化。如果不符合主动使用的条件,类就不会初始化
任何一个类型在使用之前都必须经历过完整的加载、链接和初始化 3 个类加 载步骤。一旦一个类型成功经历过这 3 个步骤之后,便“万事俱备,只欠东风”, 就等着开发者使用了。
开发人员可以在程序中访问和调用它的静态类成员信息(比如:静态字段、静 态方法),或者使用 new 关键字为其创建对象实例。
当 Sample 类被加载、链接和初始化后,它的生命周期就开始了。当代表 Sample 类的 Class 对象不再被引用,即不可触及时,Class 对象就会结束生命 周期,Sample 类在方法区内的数据也会被卸载,从而结束 Sample 类的生命周期
一个类何时结束生命周期,取决于代表它的 Class 对象何时结束生命周期
Loader1 变量和 obj 变量间接应用代表 Sample 类的 Class 对象,而 objClass 变量则直接引用它
如果程序运行过程中,将上图左侧三个引用变量都置为 null,此时 Sample 对象结束生命周期,MyClassLoader 对象结束生命周期,代表 Sample 类的 Class 对象也结束生命周期,Sample 类在方法区内的二进制数据被卸载。
当再次有需要时,会检查 Sample 类的 Class 对象是否存在,如果存在会直 接使用,不再重新加载;如果不存在 Sample 类会被重新加载,在 Java 虚拟机 的堆区会生成一个新的代表 Sample 类的 Class 实例(可以通过哈希码查看是否 是同一个实例)
综合以上三点,一个已经加载的类型被卸载的几率很小至少被卸载的时间是 不确定的。同时我们可以看的出来,开发者在开发代码时候,不应该对虚拟机的 类型卸载做任何假设的前提下,来实现系统中的特定功能。