类加载机制
类加载指的是,Java
进程运行的时候,需要把 .class
文件从硬盘读取到内存,并进行一些列的校验解析的过程(程序要想执行,就得进入内存)
.class
文件==>类对象- 硬盘==>内存
类加载过程
类加载的过程,其实是在 Java 官方文档中给出的说明
- 加载:找到. class 文件,并且读文件内容
- 验证:校验 .class 文件的格式是否符合 JVM 规范要求
- 准备:给类对象分配内存(此时内存空间全是 0 的==>类的静态成员也就是全 0 的值)
- 解析:针对类中的字符串常量进行处理
- 把类对象的各个属性进行赋值填充==>触发对父类的加载,初始化静态成员,执行静态代码块
类加载大体的过程可以分为五 个步骤(也有资料上说是三个,这个情况就是把 2,3,4 合并成一个了)
1. 加载
把硬盘上的 .class
文件找到,打开文件,读取到文件内容(认为读到的是二进制的数据)
- 找文件这里还有点幺蛾子(后面再说)
2. 验证
当前需要确保读到的文件的内容是合法的 .class
文件(字节码文件)格式
具体的验证依据,在 Java 虚拟机规范中,有明确的格式说明:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
这里的描述方式,类似与 C语言的结构体
- 前面一列是属性的类型;后面一列是属性的名字
u4
就是 4 个字节的无符号整数
magic
magic
也叫做 magic number
,魔幻数字。广泛应用于二进制文件格式中,用来标识当前二进制文件的格式是哪种类型
二进制文件是一个非常广泛的话题。
mp3
、mp4
是二进制,你图片也是二进制,你一个可执行程序也是二进制,你一个.class
文件也是二进制。不同的二进制,在使用和解析的时候肯定是存在一些差别的
所以我们一般就会在二进制文件开头指定一个固定的“魔幻数字”,通过这个数字对这个文件类型进行区分
minor/major_version
minor_version
:次版本major_version
:主版本
平时说 Java 8,9,17… 平时表达的时候使用的版本,实际上 JVM
开发过程中内部还有版本(通过 minor/major_version
进行表示)
JVM
执行 .class
文件对的时候,就会验证版本是否符合要求。如果版本不兼容,就无法执行。一般来说,高版本的 JVM
可以运行低版本的 .class
,反之不一定能行
3. 准备
给类对象申请内存空间
此时申请到的内存空间,里面的默认值,都是全 0 的。(这个阶段中,类对象里的静态成员变量的值也就相当于是 0)
4. 解析
主要是针对类中的字符串常量进行处理
解析阶段是 Java 虚拟机将常量池的符号引用替换为直接引用的过程,也就是初始化常量的过程
偏移量
class Test { private String s = "hello"; }
- 在
.class
文件中,会有一个部分用来存储“hello
”这个字符串常量(常量池里面,上面验证的格式里面包含了) - 还有一个空间对应着 s 这个变量
- 在上述代码中,我们很明显的知道,
s
变量里面相当于保存了“hello
”字符串常量的地址。但是在文件中,不存在“地址”这样的概念。谈到地址就是“内存”的地址,我们是文件,是硬盘(硬盘没有地址的概念) - 虽然没有地址,但是可以存储一个类似于地址的“
偏移量
”(文件开头到“hello
”的距离)这样的概念,用来描述这个数据的位置
符号引用和直接引用
此处文件中填充给 s
的“hello
”的偏移量,就可以认为是“符号引用”。接下来要把 .class
文件加载到内存中,就会先把“hello
”这个字符串加载到内存中,此时“hello
”就有地址了。
之后 s
里面的值就可以替换成当前“hello
”真实的地址了(直接引用)
本来在文件中存储的并非是一个真实的地址,而是一个标记(偏移量);我们回到内存中后,我们就可以把这个数据的存储换成真实的地址了
5. 初始化
针对类对象完成后续的初始化
还要执行静态代码块的逻辑,还可能会触发父类的加载