文章目录
- 定义
- 类加载过程
- 加载
- 链接
- 验证
- 准备
- 解析
- 初始化
- 类加载器
- 双亲委派模型
定义
Java 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程被称为虚拟机的类加载机制。
类加载过程
加载
Java将引用类型分为4种:类、接口、数组类和泛型参数,而泛型参数会在编译过程中被擦除。在类、接口和数组类中,数组类是由 Java 虚拟机直接生成的,其他两种则有对应的字节流。
加载,是指查找字节流,并且据此创建类的过程。
前面提到,对于数组类来说,它并没有对应的字节流,而是由 Java 虚拟机直接生成的。对于其他的类来说,Java 虚拟机则需要借助类加载器来完成查找字节流的过程。
在加载阶段,虚拟机主要完成三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
全限定类名:就是类名全称,带包路径的用点隔开,例如: java.lang.String。
链接
链接,是指将创建成的类合并至 Java 虚拟机中,使之能够执行的过程。它可分为验证、准备以及解析三个阶段。
验证
验证阶段的目的,在于确保被加载类能够满足 Java 虚拟机的约束条件,从而保证这些信息被当做代码运行后不会危害虚拟机自身的安全。
大致四项验证:
- 文件格式验证:验证字节流是否符合 Class 文件格式的规范
- 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java 语言规范》的要求(如除了 java.lang.Object 外,所有的类都应该有父类);
- 字节码验证:通过数据流分析和控制流分析,确定程序语义是合法的,符合逻辑的(如允许把子类对象赋值给父类数据类型,但不能把父类对象赋值给子类数据类型);
- 符号引用验证:验证类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。如果无法验证通过,则会抛出一个java.lang.IncompatibleClassChangeError 的子类异常,如 java.lang.NoSuchFieldError 、 java.lang.NoSuchMethodError 等。
准备
准备阶段的目的,则是为被加载类的静态字段分配内存。Java 代码中对静态字段的具体初始化,则会在稍后的初始化阶段中进行。
解析
解析阶段的目的,正是将这些符号引用解析成为实际引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)。
- 符号引用:符号引用用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
- 直接引用:直接引用是指可以直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。
整个解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这 7 类符号引用进行解析。
初始化
在 Java 代码中,如果要初始化一个静态字段,我们可以在声明时直接赋值,也可以在静态代码块中对其赋值。
如果直接赋值的静态字段被 final 所修饰,并且它的类型是基本类型或字符串时,那么该字段便会被 Java 编译器标记成常量值(ConstantValue),其初始化直接由 Java 虚拟机完成。除此之外的直接赋值操作,以及所有静态代码块中的代码,则会被 Java 编译器置于同一方法中,并把它命名为 < clinit >。
类加载的最后一步是初始化,便是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。Java 虚拟机会通过加锁来确保类的 < clinit > 方法仅被执行一次。
那么,类的初始化何时会被触发呢?JVM 规范枚举了下述多种触发情况:
- 当虚拟机启动时,初始化用户指定的主类;
- 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;
- 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
- 当遇到访问静态字段的指令时,初始化该静态字段所在的类;
- 子类的初始化会触发父类的初始化;
- 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
- 使用反射 API 对某个类进行反射调用时,初始化这个类;
- 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。
类加载器
能够通过一个类的全限定名来获取描述该类的二进制字节流的工具称为类加载器。
每一个类加载器都拥有一个独立的类名空间,因此对于任意一个类,都必须由加载它的类加载器和这个类本身来共同确立其在 Java 虚拟机中的唯一性。这意味着要想比较两个类是否相等,必须在同一类加载器加载的前提下;如果两个类的类加载器不同,则它们一定不相等。
从 Java 虚拟机角度而言,类加载器可以分为以下两类:
- 启动类加载器:启动类加载器(Bootstrap ClassLoader)由 C++ 语言实现(以 HotSpot 为例),它是虚拟机自身的一部分;
- 其他所有类的类加载器:由 Java 语言实现,独立存在于虚拟机外部,并且全部继承自 java.lang.ClassLoader 。
从开发人员角度而言,类加载器可以分为以下三类:
- 启动类加载器 (Boostrap Class Loader):负责把存放在 <JAVA_HOME>\lib 目录中,或被 -Xbootclasspath 参数所指定的路径中存放的能被 Java 虚拟机识别的类库加载到虚拟机的内存中;
- 扩展类加载器 (Extension Class Loader):负责加载 <JAVA_HOME>\lib\ext 目录中,或被 java.ext.dirs 系统变量所指定的路径中的所有类库。
- 应用程序类加载器 (Application Class Loader):负责加载用户类路径(ClassPath)上的所有的类库。
双亲委派模型
双亲委派模型的工作过程如下:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
基于双亲委派模型可以保证程序中的类在各种类加载器环境中都是同一个类,否则就有可能出现一个程序中存在两个不同的 java.lang.Object 的情况。