Java — 浅谈Java类加载机制
一. 引言:为什么Java需要类加载机制
众所周知,Java是一门兼容性强的开发语言,它由虚拟器屏蔽了各平台,操作系统的差异,具备很好的可移植性和适用性。这是Java的最大优势之一,同时我们也知道,Java是解释+编译的语言,Java文件通过javac编译成class文件,然后由jvm加载字节码,之后在运行时解释器将字节码解释成机器码执行,同时即时编译器会针对热点代码编译成机器码来获得更高的执行效率。类加载过程就是把一份被javac编译过的class文本文件通过加载生成某种形式的Class数据结构进入内存,程序可以调用这个数据结构来构造出object。这个过程是在运行时进行的,这也是Java动态拓展性的根基。
二. 综述:Java类生命周期
类的生命周期可以按上图描述分为加载,连接,初始化,使用和卸载几个过程。过程比较好理解,这里做几点说明讲解:
- 本文探究的“类加载”只包括加载,连接,初始化这三个过程。
- 需要区分“类加载”与“加载”,加载只是类加载的第一个环节。
- 解析部分是灵活的,它可以在初始化环节之后进行,实现“后期绑定”,也就是多态,其他环节顺便不可改变。
三. 细说:Java类加载过程
1. 加载
加载是读取Class文件,将其转化为某种数据结构储存在方法区内,并在堆中生成一个java.lang.Class类型的对象的过程。
这里涉及到Java内存模型,这里不细讲,后续文章会提及。
2. 连接
a. 验证
验证可以分为三部分内容,分别是:
①文件格式验证:容易理解,验证class文件的文件格式是否符合要求
②元数据,字节码验证:对class结构进行语法和语义的分析,保证其对虚拟机无害。可以理解成JVM认为该class为安全的。
③符号引用验证:它实际发生在解析阶段。
b. 准备
之后进入准备阶段,准备阶段的处理是为该类中定义的静态变量赋0值。这里顺带一提:jdk8之前类的元信息,常量池,静态变量等都放在方法区中(HotSpot用永久代实现)。而jdk8及以后常量值,静态变量直接存储在堆中,类的元信息存储在方法区(且hotSpot实现改为元空间)。
c. 解析
这个过程主要是将符号引用替换为直接引用。简单来说当一个类被编译成Class之后,如果这个A类引用了B,那么编译阶段A是不了解B是否被编译的,且B也未被加载,那么A无法找到B对应结构的地址,因此A中使用一个字符串S来代表B的地址,这个S就是符号引用,于是到了解析阶段发现B未加载就会触发B的类加载,此时A中的符号引用会被替换成B的实际地址,这称为实际引用,这样也就可以真正调用B了,这称为静态解析。
但是这里还没完,我们都知道Java的多态特性,而多态是通过动态绑定来实现的,其实也就是动态解析。假如代码中使用了多态,例如B类是一个抽象类或一个接口,有两个具体实现类C和D,那么这时并不知道A中的符号引用如何替换,需要在发生调用的时候虚拟机通过调用栈来获得具体的类型信息,从而进行解析并替换成明确的直接引用。所以这也就是之前说解析有时会发生在初始化过程后的原因,因为它实现了动态绑定,也就是invokevirtual指令。
3. 初始化
这个过程容易理解,此时会判断代码中是否存在主动的资源初始化动作,并执行之。注意这里并不是指构造方法或其他object层面的,而是class层面的,例如静态变量的赋值动作,以及静态代码块逻辑。