本文我们来研究一个Java字节码文件(Class文件)是如何加载入内存中的,在這個过程中涉及类加载过程中的加载,验证,准备,解析(连接),初始化,使用,销毁过程,并探讨实行这些过程的类加载器,以及其加载的逻辑。
概述Java拥有动态加载类和动态连接的特性,因此其加载过程并不像其他语言在编译时就已经完成,它是动态进行的,即在程序运行过程中动态加载入内存中。
加载过程在这里需要记住的是,图中的顺序说明的是阶段开始的顺序,并不是后面的阶段需要等到前面的执行完成后才能够执行,其在运行过程中是一个交叉混合执行的过程。
此外解析阶段也是一个特殊的阶段,为了支持Java语言的动态绑定,很多时候 Java 只要在运行后才能知道实际调用的对象是什么,因此解析阶段有时是开始在初始化后的。
加载加载阶段完成的是将虚拟机外部的二进制字节流按照虚拟机所需的格式存储在方法区之中。而为了完成这步需要完成哪些功能呢:
通过一个类的全限定名获取二进制流;
将二进制流定义的静态存储结构转化为方法区的运行时数据结构;
在内存中生成一个代表这个类的 Class 对象,作为方法区数据的访问接口。
需要注意的是,上面所说的3个步骤,都只是规范要求的部分,这个要求其实是比较松的,很多东西并没有限制的很死,比如说第一步的获取二进制流,其并没有要求二进制流必须从Class文件获取,因此在使用过程中类的二进制流可以从网络获取,可以动态计算生成等等。
验证验证作用是确保文件的字节流包含信息符合当前虚拟机要求,保证其并不会危害虚拟机的安全。因为以前说过 Class 文件并不都是源码编译而来的,人是可以手动修改生成 Class 文件的,因此这一步验证工作就十分有必要了。那么验证都需要验证哪些地方呢:
文件格式验证
这一步主要是保证Class文件格式上符合Java信息的要求。例如文件类型,版本号,常量池,常量池数据等等。。。。。。
此外在这一步字节流就会进入内存的方法区之中了,后面的操作都是基于方法区内的存储结构进行的。
元数据验证
对字节码描述信息进行语义分析,例如类是否有父类,重载是否正确,final,abstract有没有用错等,其主要目的是对类的元数据进行语义分析,保证符合Java语言规范。
字节码验证
对数据流和控制流进行分析。例如字节码指令集的正确,程序跳转的安全。其主要目的是检查方法体内的数据安全,确保程序语义合法,符合逻辑。
符号引用验证
符号引用验证也是一个比较特殊的阶段,其为解析阶段服务(这也验证了前面所说的,这几个过程并不是依次执行完成的)。在解析过程中,虚拟机将符号引用转换为直接引用,其主要是对常量池中的各种符号引用做匹配性校验。检验内容包括以下几个:
符号引用指向的类能否找到;
指定的类有没有描述的方法和字段;
符号引用指向的各种信息的访问权限是不是对的;
。。。。。。。。。。。。。。。
准备为类变量(被static修饰的变量)分配内存并设置类变量初始值。
这里需要注意的是设初始值值得是为其设置零值,例如数值量的 0,boolean 值的 false 等。但是特殊情况下,如类变量是一个常量,那么在准备阶段,虚拟机就会将其设置为常量指代的值。
解析在验证阶段的符号引用验证说过解析阶段就是将符号引用转换为直接引用,那么符号引用和直接引用分别指什么呢,他们之间又有何区别:
符号引用。是能够无歧义定位目标的任何形式的字面量,其与虚拟机实现的内存布局无关,引用的目标不一定需要加载入内存中;
直接引用。可以直接指向目标指针,偏移量的引用,其和虚拟机实现的内存布局相关,引用的目标一定需要在内存中。
在这一步虚拟机会将类/接口,字段,类方法,接口方法等进行解析,变为直接引用。
初始化初始化阶段主要是初始化类变量和其他资源,主要是通过<clint>()方法。
<clint>()是通过编译器自动收集所有类变量的赋值动作和静态语句块(static{}块)并按照顺序合并生成的。
static块可以为前面未定义的变量赋值,但无法访问
static{ i = 111; // 下面语句无法编译通过,会提示Illegal forward reference // System。out。println(i); } static int i = 0; public static void main(String[] args){ System。out。println(i);` }程序输出为0,因为其初始化操作是按照顺序进行的,但如果这里static int i;,不为其赋值,那么结果就是111。