本部分整理自深入理解 JVM 虚拟机
类的生命周期与加载时机
类的生命周期
一个类从被加载到虚拟机内存中开始, 到被卸载出内存为止, 整个生命周期包括了 加载, 验证, 准备, 解析, 初始化, 使用和卸载 7 个阶段. 其中 验证, 准备, 解析 3 部分统称为链接, 如下图:
整个顺序并不是完全固定的, 其中解析阶段可以在初始化之后再开始, 这样便可以实现 Java 的运行时绑定 (动态绑定) 机制.
类的加载时机
JVM 虚拟机规范并没有对类的加载时机做出严格的要求, 只规定了以下五种情况需要立刻触发类的初始化:
遇到 new,getstatic,putstatic 和 invokestatic 这四个字节码指令时, 如果类没有进行过初始化, 则需要先触发其初始化.
使用反射机制对类进行调用的时候, 如果类没有进行过初始化, 则需要先触发其初始化.
当初始化一个类时, 如果其父类还没有进行过初始化, 则需要先触发其父类的初始化.
虚拟机启动时, 用户需要指定一个要执行的主类(包含 main 方法), 此时会先初始化这个类
使用 JDK1.7 的动态语言支持时, 如果一个 MethodHandle 实例最后的解析结果包含 REF_getStatic,REF_putStatic,REF_invokeStatic 的方法句柄, 且这个方法句柄对应的类没有初始化, 则需要先对其进行初始化.
其余条件下, 可以由 JVM 虚拟机自行决定何时去加载一个类.
主动引用和被动引用
上面五种条件也被称为对类的主动引用, 除此之外其他引用类的方式都不会触发初始化, 即类的被动引用, 举个例子:
- public class Father {
- static {
- System.out.println("father init.");
- }
- public static int val = 123;
- }
- public class Son extends Father {
- static {
- System.out.println("son init.");
- }
- }
复制代码
当我们访问 Son.val 时, 会发现并没有输出 son init.
对于静态字段, 只有直接定义这个字段的类才会被初始化, 因此通过子类来引用父类的静态字段, 子类相当于是被动引用, 也就不会被初始化了.
类的加载过程
下面简单的介绍一下整个加载过程中, 每个阶段 JVM 都执行了什么操作:
加载(Loading)
加载过程是 Java 的一大特点, 类的来源可以多种多样, 压缩包, 网络字节流, 运行时动态计算生成 (reflect) 等等... 这也造就了 Java 语言强大的动态特性.
通过一个类的完整限定名来获取定义此类的二进制字节流(注意, 字节流的来源非常灵活)
将这个字节流所代表的静态储存结构转换成为方法区的运行时数据结构
在内存中生成一个代表这个类的 java.lang.Class 对象, 作为方法区这个类的各种数据的访问入口
验证(Verification)
这一过程主要是为了确保 Class 的字节流中包含的信息符合虚拟机标准, 以免造成破坏
文件格式验证
元数据验证
字节码验证, 通过数据流和控制流分析确定程序的语义是合法的
符号引用验证, 确保解析动作能够正常执行
准备(Preparation)
这一阶段将会为类变量分配内存并设置其初始值, 注意此时进行内存分配的仅包括类变量(static 修饰), 并且初始值通常情况下是数据类型的零值而不是设定值, 如下例
public static int val = 123;
复制代码
在这一阶段变量 val 的赋值是 0 而不是 123, 因为此时尚未执行任何 Java 方法, 而对 val 复制的 putstatic 指令在初始化阶段后才会执行.
当然也有特殊情况, 如下
public static final int val = 123;
复制代码
加上 final 关键字修饰后, Java 编译时会为 val 生成 ConstantValue 属性, 这时准备阶段就会根据设置将其值设置为 123.
解析(Resolution)
此阶段虚拟机将常量池内的符号替换为直接引用, 主要包含以下动作:
类或接口的解析
字段解析
类方法解析
接口方法解析
初始化(Initialization)
这时类加载过程的最后一步, 这部分开始真正的执行 Java 代码, 也就是说, 这个阶段可以由程序员参与.
此阶段其实就是执行类构造器 < clinit>()方法的过程.
类加载器
类加载器 (Class Loader) 是 Java 虚拟机的一大创举, 它将 "获取类的二进制字节流" 这个过程交给了开发人员自己去实现, 只要编写不同的 Class Loader, 应用程序本身就可以用相应的方式来获取自己需要的类.
类与加载器的关系
对于任意一个类, 都需要由加载它的类加载器和这个类本身一同确立其在虚拟机中的唯一性.
通俗的讲, 就是即便同一个 Class 文件, 被不同的类加载器加载之后, 得到也不是同一个 "类"(equals 方法返回 false).
双亲委派模型
从虚拟机角度讲, 只有两种类加载器, 一种是启动类加载器(Bootstrap ClassLoader), 在 hotpot 上使用 C++ 实现, 属于虚拟机的一部分; 另一种则是所有其他类的加载器, 这些加载器是独立于虚拟机的, 由 Java 语言实现的, 从开发者角度看, 可以分为以下两类:
扩展类加载器(Extension ClassLoader)
应用程序类加载器(Appliaction ClassLoader)
当然开发人员也可以自己编写类加载器, 最终不同的类加载器之间的层次关系如下图所示:
这就是 Java 中著名的双亲委派模型, 它要求除了顶级的 BootStrap 加载器之外, 其他类加载器都必须有父类加载器, 工作流程如下:
如果一个类加载器收到了类加载的请求, 他首先不会自己去尝试加载这个类, 而是将这个请求委派给父类加载器去完成, 只有当父加载器反馈自己无法完成加载请求时, 子加载器才会自己去尝试加载这个类.
这样做的好处是, Java 类随着它的类加载器一起具备了一种带有优先级的层次关系. 举个例子, 比如 java.lang.Object 这个类, 无论哪个类加载器加载时, 最终都会委派给 Bootstrap 加载器去加载, 这就保证了整个系统运行过程中的 Object 都是同一个类.
否则, 如果用户自己编写了一个 java.lang.Object 类, 并放在程序的 classpath 中, 最终系统将会出现多个不同的 Object 类, 整个 Java 体系就变得一团混乱了.
来源: https://juejin.im/post/5b7bb4f0e51d4538e710838f