上一篇我们已经看过了Class文件的格式,认识了Class文件中描述的各类信息,但Class文件最终都需要被加载虚拟机中之后才能被运行和使用,下面就来看一下JVM虚拟机是如何加载这些Class文件的
首先来谈谈虚拟机的类加载机制
虚拟机的类加载机制是指虚拟机将描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这一整个过程就称为类加载机制
在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的,与编译是完全分开的,这样的设计让类加载过程产生了额外的消耗、增加了一些性能开销,并且让Java语言进行提前编译会面临额外的困难;但这样设计却给Java应用提供了极高的扩展性和灵活性,比如Java的动态扩展特性就是依赖这个设计而实现的,依赖于运行期动态加载和动态连接这个特点来实现的;比如去编写一个面向接口的程序,可以等多程序运行时再指定接口对应的实现类(多态)
一个类从被加载到虚拟机内存中开始,到卸载出内存为止,整个过程(不单单只是类加载过程),也可以说是整个生命周期将会经历七个阶段
-
加载(Loading)
-
验证(Verification)
-
准备(Preparation)
-
解析(Resolution)
-
初始化(Intialization)
-
使用(Using)
-
卸载(Unloading)
其中验证、准备、解析三步部分统称为连接(Linking)
其中对前3部分,顺序是一定的,也就是一定是先执行加载、然后验证、然后准备,但到了解析过程,就不一定了,在某些情况下,解析过程会和初始化过程替换,也就是先初始化了,然后再解析,之所以这样做,是因为要支持Java语言的运行时绑定特性(也就是所谓的动态绑定,后面再详细介绍)
但这里说的按顺序执行,仅仅只是开始的顺序,并不是完成了加载、然后再进行验证,这样按顺序完成,因为有可能这些阶段会互相交叉地混合进行的,会在前面一个阶段执行的过程中调用、激活了另一个阶段,比如加载开始了,但在加载的过程中激活了验证过程
而类加载过程指的是前面5个阶段,如果归纳为连接阶段,则只有加载、连接、初始化这三个阶段
下面来说明一下类加载的时机
对于第一个阶段加载,虚拟机规范并没有进行强制约束,这点是交由虚拟机的具体实现来自由掌握的
但对于初始化阶段,虚拟机规范则是严格地规定了有且仅有六种情况必须对类进行初始化(而加载、验证、准备这三个过程就肯定是在初始化之前发生)
如下六种
- 遇到了new、getStatic、putStatic或者invokeStatic这4条字节码指令时,如果对应的类型没有进行过初始化,则会先触发其初始化阶段,这4条指令分别在下面的Java场景中生成
-
使用new关键字去实例化对象,产生new指令
-
读取或设置一个静态类型的字段,产生getStatic命令(但并不是所有的静态类型字段,如果被final修饰,已在编译器就把结果放入常量池的静态字段,是不是生成getStatic命令的)
-
使用一个类型的静态方法的时候,产生invokeStatic命令
-
使用反射特性对类型进行反射调用的时候,如果类型还没进行过初始化,则需要先触发该类型初始化
-
当初始化类的时候,发现其父类还没有进行过初始化,也需要先触发其父类的初始化
-
当虚拟机启动的时候,用户需要指定一个主函数,即main方法,虚拟机启动的时候会对主函数所在的类进行初始化
-
当使用JDK7新加入的动态语言支持时,如果一个MethodHandler实例最后的解析结果为REF_getStatic,REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的句柄时,并且这个方法句柄对应的类还没有进行过初始化,需要先触发该类初始化
-
当一个接口中定义了JDK8的新加入的默认方法(default关键字修饰的接口方法),如果这个接口的实现类发生了初始化,那该接口要在其之前被初始化(难道不用default方法,接口就不会提前实现类先初始化了?????)
上面已经分析过类在什么时候进行加载了,下面就看一下类加载的过程
加载
加载阶段仅仅只是类加载过程中的一个阶段,在加载阶段,JVM虚拟机主要完成以下三件事情
-
通过一个类的全限定名来获取定义该类的二进制字节流
-
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构(所以类的信息都存放在方法区中)
-
在内存中生成一个代表这个类的Java.lang.Class对象,作为方法区这个类的各种数据的访问入口
对于第一步,虚拟机规范里面并没有限定死二进制字节流一定从Class文件中取出,确切地说压根没有指明要从那里获取、如何获取