三. 类加载机制
参考地址:
3.1 类的加载过程
Java 类的加载过程主要分为五步:加载、验证、准备、解析、初始化。其中验证、准备、解析可以合称为连接。此外,这五步的顺序并不是完全固定的,比如为了支持动态绑定,解析的过程可以放在初始化之后。类的加载过程如下图所示:
3.1.1 加载
加载过程主要做三件事情:
- 根据全类名获取 *.class 文件的路径,通过二进制流读入 JVM 的方法区;
- 在方法区中将该字节流转为方法区的运行时数据结构;
- 在堆中生成代表该类的 java.lang.Class 对象,Class 对象的实例作为访问方法区中运行时数据结构的访问入口;
3.1.2 校验
校验阶段主要确保 Class 文件字节流中的内容不会违反当前 JVM 的规范,不会危害到 JVM 运行时的安全。主要验证的有文件格式、元数据、字节码、符号引用。
3.1.3 准备
准备阶段主要是将为类变量分配内存,并初始化为默认值。以下面的片段为例:
public static int value = 111;
需要注意的是,在准备阶段对于 int 类型,初始默认值为 0 而不是 111。同样的,其他基本类型的初始默认值都是该基本类型的默认值(如 double 的 0.0)。将 value 赋值为 111 的操作在初始化的步骤(即 clinit 方法)中进行。
3.1.4 解析
解析是将符号引用转换为直接引用的过程。
- 符号引用:一组用于标识类型的符号,符合 Java 虚拟机规范的常量表,例如其中一项常量池项目类型如下图所示;
- 直接引用:在内存中能够唯一标识对象的引用。可以是内存指针、偏移量、或者是能间接定位到目标的句柄等。
3.1.5 初始化
执行类构建方法 clinit 的过程。clinit 方法由所有类变量的赋值动作和静态语句块 static{} 合并而来,这其中也包含了父类的 clinit 方法(类变量赋值动作与父类的静态语句块),同时在执行一个类的 clinit 方法时,也会通过递归方式保证其父类的 clinit 方法先被调用。
此外对于初始化阶段,只有几种情况才会要求类立刻执行 clinit 方法:
- new:new 关键字某个未被初始化的类;
- 父类:初始化某子类时,父类未被初始化,则先初始化父类;
- 反射:通过反射调用某个未初始化的类;
- main 方法所在类;
3.2 类加载器
ClassLoader 中有一个 ClassLoader parent,记录其父类加载器。根类加载器 bootstrap ClassLoader 是最顶层的 ClassLoader,没有父类加载器。类加载器的加载范围不同,如果子类加载器想要加载父类加载器已经加载的类,可以通过双亲委派机制,直接访问父类加载器已经加载的类。
但是有的时候父类加载器也需要加载子类加载器的 Class,这时候就需要打破双亲委派机制,主要方式是使用 Thread 类里的线程上下文类加载器的方法 setContextClassLoader。