JVM - 类加载过程
字节码
在聊 Java 类加载机制之前,需要先了解一下 Java 字节码,因为它和类加载机制息息相关。
计算机只认识 0 和 1,所以任何语言编写的程序都需要编译成机器码才能被计算机理解,然后执行,Java 也不例外。
Java 在诞生的时候喊出了一个非常牛逼的口号:”Write Once, Run Anywhere”,为了达成这个目的,Sun 公司发布了许多可以在不同平台(Windows、Linux)上运行的 Java 虚拟机(JVM)——负责载入和执行 Java 编译后的字节码。
我们借助一段简单的代码来看一看,源码如下:
1 |
|
代码编译过后,通过十六进制工具 xxd JvmDemo.class命令查看这个字节码文件:
1 |
|
这段字节码中的 cafe babe
被称为“魔数”,是 JVM 识别 .class 文件的标志。文件格式的定制者可以自由选择魔数值(只要没用过),比如说 .png 文件的魔数是 89504e47
。
类加载过程
JVM结束生命周期的几种情况:
- 执行了System.exit()方法
- 程序正常执行结束
- 程序执行过程中遇到异常或错误而异常终止
- 操作系统出现错误而导致JVM进程终止
Java 的类加载过程可以分为 5 个阶段:载入、验证、准备、解析和初始化。这 5 个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。
加载
查找并加载类的二进制数据。
将类的.class文件中的二进制数据读入到内存,将其放在运行时数据区的方法区内,然后在堆去创建java.lang.Class对象,用来封装类在方法区内的数据结构。
加载 .class 的方式
- 从本地加载
- 从网络上加载(URLClassLoader(URL[] urls))
- 从 zip, jar 等归档文件中加载 .class 文件
- 从专有数据库提取 .class 文件
- 将 Java 源文件动态编译成 .class 文件
连接
验证
确保被加载的类的正确性,符合JVM字节码规范,该阶段是保证 JVM 安全的重要屏障,下面是一些主要的检查:
- 确保二进制字节流格式符合预期(比如说是否以
cafe bene
开头)。 - 是否所有方法都遵守访问控制关键字的限定。
- 方法调用的参数个数和类型是否正确。
- 确保变量在使用之前被正确初始化了。
- 检查变量是否被赋予恰当类型的值。
准备
JVM 会在该阶段对类变量(也称为静态变量, static
关键字修饰的)分配内存并初始化(对应数据类型的默认初始值,如 0、0L、null、false 等)。
1 |
|
blackist不会被分配内存,而 note会;但 bnote的初始值不是“王二”而是 null
。
需要注意的是, static final
修饰的变量被称作为常量,和类变量不同。常量一旦赋值就不会改变了,所以 bnote 在准备阶段的值为“沉默王二”而不是 null
。
解析
该阶段将常量池中的符号引用转化为直接引用。
符号引用以一组符号(任何形式的字面量,只要在使用时能够无歧义的定位到目标即可)来描述所引用的目标。
在编译时,Java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如 org.blackist.Quiz
类引用了 org.blackist.Bnote
类,编译时 Quiz类并不知道 Bnote类的实际内存地址,因此只能使用符号 org.blackist.Bnote
。
直接引用通过对符号引用进行解析,找到引用的实际内存地址。
初始化
类变量已经被赋过默认初始值,而在初始化阶段为类的静态变量赋予正确的初始值。
1 |
|
换句话说,初始化阶段是执行类构造器方法的过程。
Java程序对类的使用
主动使用(六种)
- 创建类的实例(new Test();)
- 访问某个类的或接口的静态变量,或对该静态变量赋值(int b = Test.a; Test.a = b;)
- 调用类的静态方法 (Test.foo())
- 反射(ClassForName(“org.blackst.demo.Quiz”))
- 初始化类的子类
1 |
|
- JVM启动时被标为启动类的类(如JavaTest,java org.blackist.Test)
所有JVM实现必须在每个类或接口被Java程序 首次主动使用 时才初始化。
被动使用
除了主动使用以外的使用,都不会导致类的初始化。
示例
如下程序:
输出:
1 |
|
(完)