虚拟机类加载机制 1.概述 虚拟机把描述类的数据从Class文件加载到内存中,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
1 2 3 4 5 在Java中数据类型分为基本数据类型和引用数据类型。基本数据类型有虚拟机预先定义,引用数据类型需要进行类的加载。 在Java语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为java应用程序提供高度的灵活性。 Java作为可以动态扩展的语言这种特性,依赖于运行期动态加载和动态连接这个特点实现的。
2.类生命周期 1 2 3 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)。 其中验证、准备、解析3个部分统称为连接(Linking)。
1 在类的生命周期中,加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。
3.Loading(加载)阶段 1 2 3 4 5 # 加载 就是将java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型-类模板对象。# 类模板对象 其实就是将Java类在jvm内存中的一个快照。jvm将从字节码文件中解析出的常量池、类字段、类方法等信息存储在类模板中,这样JVM在运行期便能通过类模板而获取Java类中的任意信息,能够对Java类的成员变量进行遍历,也能进行Java方法的调用。 反射的机制即基于这一基础。如果JVM没有将java类的声明信息存储起来,则JVM在运行期也无法反射。
1 2 3 4 在加载阶段,虚拟机完成以下3件事1. 通过一个类的全限定名来获取定义此类的二进制字节流2. 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
1 2 3 4 5 6 7 8 二进制流的获取方式: 对于类的二进制数据流,虚拟机可以通过多种途径产生或获得。1. 虚拟机可能通过文件系统读入一个class后缀的文件(最常见)2. 读入jar、zip、war等数据包,提取类文件。3. 从网络中获取,applet4. 运行时计算生成,这种场景使用得最多的就是动态代理技术。5. 有其他文件生成,典型场景JSP应用。6. 从数据库中读取
1 2 3 4 5 6 7 # 生成的类模板的位置 加载的类在JVM中创建相应的类结构,类结构会存储在方法区(java1.8之前:永久代,java1.8之后:元空间)# Class实例的位置 类将class文件加载至方法区之后,会在堆中创建一个Java.lang.Class对象,用来封装类位于方法区内的数据结构,该Class对象是在加载类的过程中创建的,每个类都对应有一个Class类型的对象,这个对象将作为程序访问方法区中的这些类型数据的外部接口。 Class类的构造方法是私有的,只有JVM能够创建。
4.Linking(连接阶段) 1 加载阶段于连接阶段的部分内容是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。
4.1验证阶段(Verification) 1 验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息复合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
1 验证阶段大致上会完成文件格式验证、元数据验证、字节码验证、符号引用验证
文件格式验证
1 2 3 4 5 6 7 8 9 10 # 文件格式验证 是否是以魔数0xCAFEBAVE 主、次版本号是否是当前虚拟机处理范围之内 常量池的常量中是否有不被支持的常量类型(检查常量tag标志) 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量 ......# 文件格式验证的目的 保证输入的字节流能正确地解析并存储与方法区之内,格式上符合描述一个Java类型信息的要求。 这阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,所以后面的3个验证阶段全部是基于方法去的存储结构进行的,不会再直接操作字节流。
元数据验证
1 2 3 4 5 6 7 8 9 # 元数据验证 是否继承final 是否有父类 如果这个类不是抽象类,是否实现了父类或接口之中要求实现的所有方法 类中的字段、方法是否与父类产生了矛盾 ......# 元数据验证的目的 对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。
字节码验证
1 2 3 4 5 6 7 8 9 # 字节码验证 跳转指令是否指向正确的位置 操作数类型是否合理 ......# 字节码验证的目的 通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。 如果一个类方法体的字节码没有通过字节码验证,那肯定是有问题的,但如果一个类方法体的字节码通过了字节码验证,也不能说明其一定安全。
符号引用验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 符号引用验证 符号引用的直接引用是否存在 ......# 什么是符号引用 以一组符号来描述所引用的目标,付哈可以是任何形式的字面量,只要使用能无歧义地定位到目标即可。# 什么是直接引用 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。# 符号引用的发生时间 符号引用的发生时间在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段--解析阶段中发生。# 符号引用的目的 对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。
4.2准备(Preparation)
1 2 3 4 例外: 如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,比如 public static final int value = 123; 编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue赋值为123
4.3解析(Resolution) 1 解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程,符号引用在Class文件中它以CONSTANT_Class_ info、CONSTANT_Fieldref_ info等类型的常量出现。
5.初始化(Initialization) 1 2 3 4 类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户引用程序可以通过自定义类加载参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的java程序代码。# 初始化目的 执行类的初始化方法: <clinit > ()方法。
1 2 3 # <clinit > ()方法 1. 该方法仅能由java编译器生成并由JVM调用,程序开发者无法自定义一个同名的方法,更无法在Java程序中调用该方法,虽然该方法也是有字节码指令所组成。2. 它是由类静态成员的赋值语句以及static语句块合并产生的。
1 2 # 注意: 在加载一个类之前,虚拟机总会试图加载该类的父类,因此父类的<clinit > ()方法总是在子类<clinit > ()方法之前被调用
1 2 # <clinit > ()方法的线程安全性 虚拟机会保证一个类的<clinit > ()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit > ()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit > ()方法完毕。
1 2 3 4 # 问题: 类是否初始化? 在代码中,有些类初始化了,有些类没有,需要对那些类进行初始化? 我们先需要了解主动使用和被动使用
1 2 3 4 5 6 7 8 9 10 11 ## 主动使用(会发生初始化) Class只有在必须要首次使用的时候才会被装载,java虚拟机不会无条件地装载Class类型。 java虚拟机规定,一个类或接口在初次使用前,必须要进行初始化。这里的"使用"是指主动使用。 主动使用只有下列几种情况:1. 遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。3. 当初始化一个类的时候,如果发现其父类还没进行过初始化,则需要先触发其父类的初始化。4. 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类。
1 2 3 4 5 6 ## 被动使用(类不会初始化) 1. 通过子类引用父类的静态字段,不会导致子类初始化2. 通过数组定义类引用类,不会触发此类的初始化3. 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化