1. 概述
在Java中数据类型分为基本数据类型和引用数据类型。
- 基本数据类型由虚拟机预先定义
- 引用数据类型则需要进行类的加载。
按照Java虚拟机规范,从class文件到加载到内存中的类,到类卸载出内存为止,它的整个生命周期包括如下7个阶段:
2. 类加载的过程
- 加载,验证,准备和初始化和卸载这五个阶段的开始顺序是一定的,但是通常这些阶段都说互相交叉地混合进行的
2.1 加载阶段
2.1.1 目标
- 通过一个类的全限定名来获取此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口
2.1.2 加载的途径
- 从本地系统中直接加载
- 通过网络获取,典型场景:Web Applet
- 从zip压缩包中读取,成为日后jar、war格式的基础
- 运行时计算生成,使用最多的是:动态代理技术
- 由其他文件生成,典型场景:JSP应用从专有数据库中提取.class文件,比较少见
- 从加密文件中获取,典型的防Class文件被反编译的保护措施
2.1.3 加载的对象
对于数组类型和 非数组类型的加载方式是不一样的
创建数组类的情况稍微有些特殊,因为数组类本身并不是由类加载器负责创建,而是由JVM在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建。创建数组类(下述简称A)的过程:
- 如果数组的组件类型是引用类型,那就遵循定义的加载过程递归加载和创建数组A的元素类型,数组C将被标识在加载该组件类型的类加载器的类名称空间上
- 如果数组的组件类型不是引用类型,Java虚拟机将会把数组C标记为与引用类加载器关联
如果数组的元素类型是引用类型,数组类的可访问性就由元素类型的可访问性决定。否则数组类的可访问性将被缺省定义为public。
2.1.4 加载的方式
这里的方式主要针对的是非数组类型
- 类加载器
- 双亲委派机制
鉴于篇幅原因,可以参考本章的 3.类加载器
2.1.5 加载的效果
-
所谓加载,简而言之就是将Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型——类模板对象。
-
所谓类模板对象,其实就是Java类在]VM内存中的一个快照,JVM将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,这样]VM在运行期便能通过类模板而获取Java类中的任意信息,能够对Java类的成员变量进行遍历,也能进行Java方法的调用。
-
反射的机制即基于这一基础。如果JVM没有将Java类的声明信息存储起来,则JVM在运行期也无法反射。
类模型
加载的类在JVM中创建相应的类结构,类结构会存储在方法区(JDKl.8之前:永久代;J0Kl.8及之后:元空间)。
Class实例
-
类将.class文件加载至元空间后,会在堆中创建一个Java.lang.Class对象,用来封装类位于方法区内的数据结构,该Class对象是在加载类的过程中创建的,每个类都对应有一个Class类型的对象。
-
Class类的构造方法是私有的,只有JVM能创建
-
java.lang.Class实例是访问类型元数据的接口,也是实现反射的关键数据、入口。通过Class类提供的接口,可以获得目标类所关联的. class文件中具体的数据结构:方法、字段等信息。
2.1.6 总结
- 从各种地方获取到Class文件的二进制流以后
- 通过类加载器和双亲委派机制将流加载进内存中
- 这些内容是存储在方法区,形成了类文件模板。类文件模板包含了常量池,类方法,类字段等信息
- 之后根据这个模板,在堆区创建一个Class对象
2.2 链接阶段
链接分为三个子阶段:验证 -> 准备 -> 解析
2.2.1 验证阶段
当类加载到系统后,就开始链接操作,验证是链接操作的第一步。验证阶段很重要,但是不是必须的,可以进行关闭
整体说明:
验证的内容则涵盖了类数据信息的格式验证、语义检查、字节码验证,以及符号引用验证等。
- 其中格式验证会和加载阶段一起执行。验证通过之后,类加载器才会成功将类的二进制数据信息加载到方法区中。
- 格式验证之外的验证操作将会在方法区中进行。
链接阶段的验证虽然拖慢了加载速度,但是它避免了在字节码运行时还需要进行各种检查。(磨刀不误砍柴工)
目的
它的目的是保证加载的字节码是合法、合理并符合规范的,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
验证的步骤比较复杂,实际要验证的项目也很繁多,大体上Java虚拟机需要做以下检查,如图所示。
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
文件格式验证
- 是否以魔数0XCAFEBABE开头,
- 主版本和副版本号是否在当前Java虚拟机的支持范围内
- 常量池的常量中是否有不被支持的常量类型
- 指向常量的各种索引值是否有指向不存在的常量或不符合类型的常量
- CONSTANT_UTF8_info型的常量中是否有不复合UTF-8编码的数据
- Class文件中各部分以及文件本身是否有被删除的或附加的其他信息
- 数据中每一个项是否都拥有正确的长度等。
- 等等
元数据验证
Java虚拟机会进行字节码的语义检查,但凡在语义上不符合规范的,虚拟机也不会给予验证通过。比如:
- 是否所有的类都有父类的存在(在Java里,除了object外,其他类都应该有父类)
- 是否一些被定义为final的方法或者类被重写或继承了
- 非抽象类是否实现了所有抽象方法或者接口方法
- 类中的字段,方法是否与父类产生矛盾
字节码验证
Java虚拟机还会进行字节码验证,字节码验证也是验证过程中最为复杂的一个过程。它试图通过对字节码流的分析,判断字节码是否可以被正确地执行。比如:
- 在字节码的执行过程中,是否会跳转到一条不存在的指令
- 函数的调用是否传递了正确类型的参数
- 变量的赋值是不是给了正确的数据类型等
栈映射帧(StackMapTable)就是在这个阶段,用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型。但遗憾的是,100%准确地判断一段字节码是否可以被安全执行是无法实现的,因此,该过程只是尽可能地检查出可以预知的明显的问题。如果在这个阶段无法通过检查,虚拟机也不会正确装载这个类。但是,如果通过了这个阶段的检查,也不能说明这个类是完全没有问题的。
在前面3次检查中,已经排除了文件格式错误、语义错误以及字节码的不正确性。但是依然不能确保类是没有问题的。
符号引用验证
验证行为发生在将符号引用转化为直接引用的时候
校验器还将进符号引用的验证。Class文件在其常量池会通过字符串记录自己将要使用的其他类或者方法。因此,在验证阶段,虚拟机就会检查这些类或者方法确实是存在的,并且当前类有权限访问这些数据,如果一个需要使用类无法在系统中找到,则会抛出NoClassDefFoundError,如果一个方法无法被找到,则会抛出NoSuchMethodError。此阶段在解析环节才会执行。
2.2.2 准备阶段
目的
准备阶段(Preparation),简言之,为类的静态变分配内存,并将其初始化为默认值。
当一个类验证通过时,虚拟机就会进入准备阶段。在这个阶段,虚拟机就会为这个类分配相应的内存空间,并设置默认初始值。Java虚拟机为各类型变量默认的初始值如表所示。
类型 | 默认初始值 |
---|---|
byte | (byte)0 |
short | (short)0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0 |
char | \u0000 |
boolean | false |
reference | null |
Java并不支持boolean类型,对于boolean类型,内部实现是int,由于int的默认值是0,故对应的,boolean的默认值就是false。
注意
-
这里不包含基本数据类型的字段用static final修饰的情况,因为final在编译的时候就会分配了,准备阶段会显式赋值。
// 一般情况:static final修饰的基本数据类型、字符串类型字面量会在准备阶段赋值 private static final String str = "Hello world"; // 特殊情况:static final修饰的引用类型不会在准备阶段赋值,而是在初始化阶段赋值 private static final String str = new String("Hello world");
-
注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
-
在这个阶段并不会像初始化阶段中那样会有初始化或者代码被执行。
2.2.3 解析阶段
目的
将符号引用转化为直接引用
符号引用
- 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标(这也是符号引用的存在意义)
- 符号引用与虚拟机实现的内存布局无关,,引用的目标不一定被加载到虚拟机中
直接引用
- 可以直接指向目标的指针,相对偏移量或者是一个能间接定位到目标的句柄
- 和虚拟机内存布局直接相关的。
- 有了直接引用,那么目标一定在虚拟机的内存中
解析时间
解析时间没有明确规定,但是一定要在这17条指令执行前进行解析
anewarray,checkcast,getfield,getstatic,instanceof,invokexxx,ldc,ldc_w,ldc2_w,multianewarray,new,putfield
- 可以在类被加载器加载时就对常量池中的符号引用解析
- 也可以等使用前才进行解析
缓存
除invokedynamic指令以外,虚拟机实现可以对第一次解析的结果进行缓存。因为invokedynamic指令的目的本来就是用于动态语言的支持。
具体过程
见《深入理解Java虚拟机》274-276
2.3 初始化阶段
2.3.1 定义
初始化阶段就是执行方法的过程
- 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。也就是说,当我们代码中包含static变量的时候,就会有clinit方法
<clinit>()
方法中的指令按语句在源文件中出现的顺序执行<clinit>()
不同于类的构造器。(关联:构造器是虚拟机视角下的<init>()
)- 若该类具有父类,JVM会保证子类的
<clinit>()
执行前,父类的<clinit>()
已经执行完毕
- 接口的实现类不需要先执行接口的clinit
- 接口的父接口不需要先执行clinit
- 只有当使用了才需要clinit
- 虚拟机必须保证一个类的
<clinit>()
方法在多线程下被同步加锁- clinit只执行一次
2.3.2 static和final搭配
说明:使用static+ final修饰的字段的显式赋值的操作,到底是在哪个阶段进行的赋值?
- 情况1:在链接阶段的准备环节赋值
- 情况2:在初始化阶段<clinit>()中赋值
结论: 在链接阶段的准备环节赋值的情况:
-
对于基本数据类型的字段来说,如果使用static final修饰,则显式赋值(直接赋值常量,而非调用方法)通常是在链接阶段的准备环节进行 如果调用方法来赋值 是在clinit中初始化
-
对于String来说,如果使用字面量的方式赋值,使用static final修饰的话,则显式赋值通常是在链接阶段的准备环节进行
-
在初始化阶段<clinit>()中赋值的情况: 排除上述的在准备环节赋值的情况之外的情况。
使用static+final修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类型 或 String类型的显式赋值,是在链接阶段的准备环节进行。否则是在初始化阶段赋值
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>()中赋值
2.3.3 主动使用
概念
并不是所有类的加载都会进行初始化阶段,只有主动使用的情况下才会立刻触发初始化
- 遇到new,getstatic,putstatic,invokestatic这四条字节码指令时,如果类型没用进行初始化,则需要先触发其初始化阶段。能够生成这四条指令的场景有
- 使用new关键词实例化对象的时候
- 读取或设置一个类型的静态字段的时候(被final修饰,已再编译期把结果放入常量池的静态字段除外)
- 调用一个类型的静态方法的时候
- 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行初始化,则需要先初始化
- 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化主类
- 当使用JDK7新加入的动态语言支持时,。。。。。
- 当一个接口中定义了JDK8新加入的默认方法,如果有这个接口的实现类发生了初始化,那该接口要在其之前初始化
具体例子
-
实例化:当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化。
/** * 反序列化 */ Class Order implements Serializable { static { System.out.println("Order类的初始化"); } } public void test() { ObjectOutputStream oos = null; ObjectInputStream ois = null; try { // 序列化 oos = new ObjectOutputStream(new FileOutputStream("order.dat")); oos.writeObject(new Order()); // 反序列化 ois = new ObjectInputStream(new FileOutputStream("order.dat")); Order order = ois.readObject(); } catch (IOException e){ e.printStackTrace(); } catch (ClassNotFoundException e){ e.printStackTrace(); } finally { try { if (oos != null) { oos.close(); } if (ois != null) { ois.close(); } } catch (IOException e){ e