写在前面
我们知道我们编写的 java 代码, 会经过编译器编译成字节码文件(class 文件), 再把字节码文件装载到 JVM 中, 映射到各个内存区域中, 我们的程序就可以在内存中运行了. 那么字节码文件是怎样装载到 JVM 中的呢? 中间经过了哪些步骤? 常说的双亲委派模式又是怎么回事? 本文主要搞清楚这些问题.
类装载流程
1, 加载
加载是类装载的第一步, 首先通过 class 文件的路径读取到二进制流, 解析二进制流将里面数据结构 (类型, 常量等) 载入到方法区, 在 java 堆中生成对应的 java.lang.Class 对象用类封装类在方法区中的数据结构.
2.1, 验证
验证的主要目的就是判断 class 文件的合法性, 比如 class 文件一定是以 0xCAFEBABE 开头的, 另外对版本号也会做验证, 例如如果使用 java1.8 编译后的 class 文件要再 java1.6 虚拟机上运行, 因为版本问题就会验证不通过. 除此之外还会对元数据, 字节码进行验证, 机构验证, 语义验证, 字节码验证.
2.2, 准备
准备过程就是分配内存, 给类的一些字段设置初始值, 例如: public static int v=1;
这段代码在准备阶段 v 的值就会被初始化为 0, 只有到后面类初始化阶段时才会被设置为 1.
但是对于 static final(常量), 在准备阶段就会被设置成指定的值, 例如: public static final int v=1;
这段代码在准备阶段 v 的值就是 1.
对于 int 类型的静态变量分配 4 个字节的内存空间, 并且默认值为 0.long 类型的静态变量分配 8 个字节的内存空间, 默认值为 0. 布尔(false)
2.3, 解析
解析过程就是将符号引用替换为直接引用, 例如某个类继承 java.lang.object, 原来的符号引用记录的是 "java.lang.object" 这个符号, 凭借这个符号并不能找到 java.lang.object 这个对象在哪里? 而直接引用就是要找到 java.lang.object 所在的内存地址, 建立直接引用关系, 这样就方便查询到具体对象. 或者 A 类中调用了 B 类对象的 fun()方法, 那么 b.fun()就是符号引用, 会转换为 B 类 fun()的具体地址.
3, 初始化
初始化过程, 主要包括执行类构造方法, static 变量赋值语句, staic{}语句块, 需要注意的是如果一个子类进行初始化, 那么它会事先初始化其父类, 保证父类在子类之前被初始化. 所以其实在 java 中初始化一个类, 那么必然是先初始化 java.lang.Object, 因为所有的 java 类都继承自 java.lang.Object.
触发类初始化的场景
1. 创建类的实例.
2: 访问类或者接口的静态变量, 或者给静态变量赋值.
3. 调用类的静态方法.
4. 反射(如 Class.forName("com.a.b.c.Test"))
5. 初始化一个类的子类.
6.Java 虚拟机启动时被标记为启动类的类
系统中的 ClassLoader
Bootstrap Classloader (启动 ClassLoader) 只加载
- Extension ClassLoader (扩展 ClassLoader)
- App ClassLoader(应用 ClassLoader)
- Custom ClassLoader(自定义 ClassLoader)
每个 ClassLoader 都有另外一个 ClassLoader 作为父 ClassLoader,Bootstrap Classloader 除外, 它没有父 Classloader.ClassLoader 加载机制如下:
类的加载
类的加载并不需要等到某个类被 "首次主动使用" 时再加载它.
JVM 规范允许类加载器在预料某个类将要被使用时就预先加载它, 如果预先加载过程中遇到了. class 文件缺失或者存在错误, 类加载器必须在程序主动使用该类时报告错误(LinkageError 错误), 如果这个类一直没有被程序使用, 那么类加载器就一直不会报告这个错误.
静态常量
编译时静态常量 static final a = 6/3; // 不会触发类的初始化
允许时静态常量 static final a = Math.random(100); // 会触发类的初始化
来源: http://www.bubuko.com/infodetail-3195623.html