1.类加载的目的:
一份被javac编译过的class文本文件通过加载,生成某种形式的Class数据结构进入内存, 程序可以调用这个数据结构来构造出object,这个过程是在运行时进行的,也是java动态拓展性的根基。
一个类的生命周期:
- javac 编译
- 加载
- 链接
- 初始化
- 使用
- 卸载
类加载包含了三个阶段
- 加载
- 链接
- 初始化
链接包含三个步骤:
- 验证
- 准备
- 解析
其中,解析步骤是灵活的,他可以在初始化之前或者之后再进行,实现所谓的‘后期绑定’,其他环节则是不可改变的。
2.类加载的阶段
加载
加载是一个读取Class文件,将其转化为某种静态数据结构存储在方法区内,并在堆中生成一个便于用户调用的java.lang.Class类型的对象的过程。
注意:
这里的Class文件不仅仅是指的本地文件,泛指各种来源的二进制流,比如从网络、数据库、即时计算出来的class文件,比如著名的就是 动态代理技术,就是使用到了即时计算出来的class,然后实例化代理对象,可以自由选择二进制流。
链接
- 验证
验证动作是有很多个步骤的,包括:文件格式验证、元数据字节码验证、符号引用验证。
文件格式验证:其实是发生在加载阶段的,如果通过,那么才能顺利加载,顺利加载后此时方法区内虽然已经存在了该class的静态结构,堆中也存在了该class类型的对象,但是这并不代表着JVM已经完全认可了这个类。如果程序想要使用这个类那么就必须进行链接,而链接的第一步就是进一步对这个类进行验证,到底对方法区内的class静态结构进行了哪些方面的验证。
元数据的验证、字节码的验证:就是对class的静态结构进行语法和语义上的分析,保证其不会产生危害虚拟机的行为。这两个步骤验证通过,那么虚拟机会姑且认为该class是安全的,但是这并不意味着验证已经完全结束了,还有一道对符号引用进行验证的步骤。
符号引用验证:它是在解析阶段内发生的,而解析阶段我们之前也提到过,它可以在初始化阶段之前或者之后进行。
验证其实包含了很多的步骤,分散在各个不同的阶段内,验证的内容是会不断发展的,除了这里提到的文本格式验证、元数据字节码验证、符号引用验证 四个环节 从低版本的虚拟机到现在 验证步骤其实已经不断的加入了各种机制。在未来,虚拟机的开发人员可能会引入更多更完善更完美的策略。
- 准备
在元数据、字节码验证通过之后,虚拟机会姑且认为该class是安全的,这时候会进入准备阶段,准备阶段做的处理,并不复杂,就是为该类型中定义的静态变量赋0值,注意,这里仅仅是静态变量而不是成员变量,此时将会出现一个被太多人混淆和误解的概念,我们这里就好好捋一捋。
虚拟机内存规范中的定义了方法区这种抽象概念,HotSpot这种主流虚拟机在JDK8之前使用了永久代这种具体的实现方式来实现方法区。在JDK8之后,弃用‘永久代’这种实现方式,采用元空间这种直接内存取代。
在JDK8之前,类的元信息、常量池、静态变量等都存储在 永久代这种具体实现中,而在JDK8及之后,常量池、静态变量被移除‘方法区’,转移到了堆中,元信息这些呢,依然保留在方法区内,但是具体的存储方式改成了元空间。
- 解析
该阶段主要做的事情是将 符号引用 替换为 直接引用。什么是符号引用,什么是直接引用?
当一个Java类被编译成Class之后,假如这个类称为A,并且A中 引用了B,那么在编译阶段,A是不知道B有没有被编译的,而且此时B也一定没有被加载,所以A肯定不知道B的实际地址,那么A怎么才能找到B呢?,此时在A的class文件中,将使用一个字符串S来代表B的地址,S就被称为符号引用,在运行时呢,如果A发生了类加载,到了解析阶段会发现B还未被加载,那么将会触发B的类加载,将B加载到虚拟机中,此时A中的B的符号引用将会被替换成B的实际地址,这被称为直接引用,这样也就能够真正的调用B了。
Java通过后期绑定的方式来实现多态,那么后期绑定这个概念,又是如何实现的呢?
其实就是这里的动态解析,如果A调用的B是一个具体的实现类,那么就称为静态解析,因为解释的目标类型很明确,而假如上层Java代码使用了多态,这里的B是一个抽象类或者是接口,那么 B可能有两个具体的实现类C和D 此时,B的具体实现并不明确,当然也就不知道使用哪个具体类的直接引用来进行替换,既然不知道,那么就等一等吧,直到运行过程中发生了调用,此时虚拟机调用栈中将会得到具体的类型信息,这时候再进行解析,就能用明确的直接引用来替换符号引用,这就解释了为什么解析阶段有时候会发生在初始化阶段之后,这就是动态解析,用它来实现了后期绑定。底层对应了invokedynamic这条字节码指令。
扩展:多态
扩展:动态绑定
初始化
初始化阶段简单概括:此时会判断代码中是否存在主动的资源初始化操作,如果有的话,那么执行。这里所说的主动的资源初始化动作,不是指的构造函数,而是class层面的,比如说成员变量的赋值动作,静态变量的赋值动作,以及静态代码块的逻辑。而只有显式的调用new指令 才会调用构造函数进行对象的实例化,这是对象层面的,二者不能进行混淆。