把描述类的数据从Class文件加载到内存中 并对数据进行校验 转换解析和初始化 最终形成可以被虚拟机直接使用的java类型 这个过程称为虚拟机的类加载机制
在java语言中类型的加载 连接 初始化过程都是在程序运行期间完成的 这种策略让类加载时增加了一些性能开销 但是却为java应用提高了极高的扩展性和灵活性 java天生可以动态扩展的语言特性就是依赖运行期间动态加载和动态连接这个特点实现的
例如:编写一个面向接口的应用程序 可以等到运行时再指定其实际的实现类 用户通过java预置的或自定义的类加载器让某个本地应用程序在运行时从网络或其他地方加载一个二进制流作为其程序代码的一部分
类加载的时机
一个类型从被加载到虚拟机内存中开始 到卸载出内存为止 整个生命周期经历加载 验证 准备 解析 初始化 使用 卸载 七个阶段
加载 验证 准备 初始化 卸载这五个阶段顺序是确定的 而解析阶段则不一定 某些情况可以在初始化后再解析(互相交互混合进行)
六种情况必须立即对类进行初始化:
- 遇到new getstatic outstatic invokestatic这四条字节码指令时 场景:
1)new关键字实例化对象时
2)读取或设置一个类型的静态字段(被final修饰 已在编译器把结果放入常量池的静态字段除外)的时候
3)调用一个类型的静态方式的时候 - 使用java.lang.reflect包的方法对类型进行反射调用的时候
- 当初始化类时其父类还未初始化 需要先对父类初始化
- 虚拟机启动时 用户需要制定一个要执行的主类(包含main方法的类)
- 当使用JDK 7新加人的动态语言支持时,如果一个 java.lang.invoke MethodHandle实例最后的解析结果为REF getStatic、 REF putStatic、 REF invokeStatic、 REF newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
- 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
类加载的过程
加载
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存生成一个代表该类的java.lang.Class对象 作为方法区这个类的各种数据的访问入口
加载与连接阶段的部分动作(如一部分字节码文件格式验证)是交叉进行的
验证
连接阶段的第一步 目的是确保Class文件的字节流中包含的信息符合《java虚拟机规范》的全部约束要求
大致上分为下面四个检验动作:文件格式验证 元数据验证 字节码验证 符号引用验证
- 文件格式验证 保证输入的字节流整正确的解析并存储于方法区内 基于二进制字节流进行
- 元数据验证 对字节码描述的信息进行语义分析 保证其信息符合《java虚拟机规范》的全部约束要求 (如 该类的父类是否继承了被final修饰的类 或不是抽象类是否实现了父类或接口要求实现的所有方法等)
- 字节码验证 最复杂的阶段 目的是通过数据流分析和控制流分析 确定程序语义是合法的 对类的方法体进行校验分析
- 符号引用验证 发生在虚拟机将符号引用转化为直接引用的时候 可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验 通俗来说 :该类是否缺少或者被禁止访问他所依赖的某些外部类 方法 字段等
准备
正式为类中定义的变量(static静态变量)分配内存并设置类变量初始值(不包括实例变量 实例变量将在对象实例化时随对象一起分配在java堆中)
注意:如:public static int value = 123;
在准备阶段value值为初始值0 而不是123 只有当类被初始化 value才变为123
若public static final int value = 123;
有final则在准备阶段就将value赋值为123
解析
java虚拟机将常量池内的符号引用替换为直接引用的过程
- 符号引用: 以一组符号来描述所引用的目标 符号可以是任何形式的变量 只要使用时能无歧义的定位到目标即可 与虚拟机实现的内存布局无关
- 直接引用:可以直接指向目标的指针 相对偏移量或者是一个能间接定位到目标的句柄 与虚拟机实现的内存布局直接相关
虚拟机可以自行判断到底是在类被加载器加载时就对常量池中的符号引用进行解析 还是等到一个符号引用将要被使用前才去解析它
对一个符号引用可以进行多次解析请求
解析动作主要针对类或接口 字段 类方法 接口方法 方法类型 方法句柄 调用点限定符 这七类符号引用进行
初始化
是类加载过程的最后一个步骤 其他几个动作除了在加载阶段用户程序可以通过自定义类加载器的方式局部参与外 其余动作都完全由java虚拟机来主导 到该阶段java虚拟机才开始执行类中编写的java程序代码 将主导权移交给应用程序
在准备阶段 变量已经赋过一次系统要求的初始值 在初始化阶段则会根据程序员通过程序编码指定的主观计划去初始化类变量和其他资源(执行类构造器方法的过程< clinit >()方法)
(< clinit >()方法由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并而成的
< clinit >()方法与构造函数不同 并不需要调用父类构造器 java虚拟机保证了在子类的< clinit >()方法执行前 父类的()方法已经执行完毕 因此jvm中第一个被执行的< clinit >()方法方法的类型肯定是java.lang.Object
< clinit >()方法对于类或者接口并不是必须的 如果一个类中没有静态语句块也没有对变量的赋值操作 编译器可不生成这个类的< clinit >()方法
jvm必须保证一个类的< clinit >()方法在多线程环境中被加锁同步)
类加载器
只用与实现类的加载动作 但是对于java程序中起到的作用更大
对于任意一个类都必须由加载他的类和这个类本身一起共同确立其在java虚拟机中的唯一性 每一个类加载器都拥有一个独立的类名称空间(两个类是否相等 只有在这两个类由同一个类加载器加载的前提下才有意义 换句话说类加载器不同 这两个类必定不相等)
从jvm的角度来看只存在两种不同的类加载器
- 启动类加载器 使用c++语言实现是虚拟机的一部分
- 其他所有类的加载器 由java语言实现 独立存在于虚拟机外部 全继承自抽象类java.lang.ClassLoader