首页 > 技术文章 > Java虚拟机之类加载机制

itero 2019-06-13 22:04 原文

一个Java程序要运行,首先需要加载到内存中,磁盘中是不能直接运行程序的。我们写好的代码和编译后的字节码class文件存在磁盘中。值得一提的是,Java是在运行期间进行类的加载,称之为是动态的。也就是说,在程序运行期间,需要这个类的时候才会进行加载,这似乎浪费了点加载的时间,不过付出这点代价是值得的,换来的是程序的灵活性,灵活性越强当然功能也就越强。Java可以很容易的实现一个面向接口的程序,而且,我们也可以加载不在我们电脑中的class文件,通过读取网络中的class文件,当然这个class文件是一串二进制的字节流。

总体来看一个Java类的生命周期从加载到运行一共分为7个阶段,但是这7个阶段并不是严格的一个阶段结束下一个阶段再开始,而是会互相交叉着进行。这七个阶段如下:

既然Java程序是动态加载类的,那么就需要有一种标志去让虚拟机启动类加载过程,这种标志通常有:

  1. Java虚拟机执行字节码的过程中遇到了newgetstaticputstaticinvokestatic这4条字节码指令时,如果没有进行过初始化,就需要先触发其初始化,这是编译后的虚拟机指令,对应到Java代码里就是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段的时候,而被final修饰,已经在编译期把结果放入常量池的静态字段除外,以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当Java虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个类。

以上这几种场景称为对一个类进行主动引用,也就是说,会触发引用类初始化的引用称之为主动引用,否则称之为被动引用。

第一步,加载,加载阶段通过一个类的全限定名来获取定义此类的二进制字节流,二进制流的获取方式很灵活,可以从本机的class文件中获取;可以从ZIP包中读取,在此基础上,可以作为JAR、EAR、WAR包的技术基础;可以在网络中获取,比如Applet;可以运行时计算生成,这种场景使用的最多的就是动态代理技术,在java.lang.reflect;由其他文件生成,典型场景是JSP应用,即由JSP文件生成对应的Class类。然后将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,转换完成后,会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。转换完成后,需要进行初步校验,因为有可能在读取过程出错,转换错了数据,直接转换为Class实例会出错。需要校验的有,cafe babe魔法数、常量池、文件长度、是否有父类等。在检查完成之后,创建对应类的java.lang.Class实例。

相对于类加载的其他阶段,一个非数组类的加载阶段,也就是加载阶段中获取类的二进制字节流的动作,是开发人员可控性最强的,因为加载阶段既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式,也就是重写一个类加载器的loadClass()方法。数组类有所不同,数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。

加载阶段完成之后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义。然后在内存中实例化一个java.lang.Class类的对象,由于Java虚拟机规范并没有规定Class对象必须在堆中创建,所以HotSpot将其存放于方法区,这个对象将作为程序访问方法区中的这些类型数据的外部接口。

第二步,验证,在加载阶段尚未完成时,连接阶段可能已经开始了,验证是连接的第一步,进行更进一步的校验,比如说final是否合规、类型是否正确、静态变量是否合理等。这一阶段的目的是确保Class文件的字节流中包含的信息是否符合要求,并且不会危害虚拟机自身的安全。这个阶段的严格检查可以让虚拟机承受住恶意代码的攻击,防止虚拟机奔溃,比如访问数组边界以外的数据、将一个对象转型为它未实现的类型、跳转到不存在的代码行等等。当然直接编译这种代码编译器会报错,但是Class文件的获取是多样的,所以验证能使Java更安全。

从整体上看,验证阶段会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

第三步,准备,检查完成之后,准备阶段开始为静态变量分配内存,并设定默认值,int默认为0floatdouble默认为0.0,Bool值默认为false

第四步,解析,完成静态变量设置之后,需要对类和方法进行解析以确保类与类之间的相互引用正确性,完成内存结构布局。

第五步,初始化,在解析完成之后,准备工作就做好了,开始进行初始化。初始化阶段会执行类构造器<clinit>方法,如果赋值运算是通过其他类的静态方法来完成的,那么会马上解析另外一个类,在虚拟机栈中执行完毕后通过返回值进行赋值。

经过一系列上述步骤,就成功的将一个类加载到了内存中,可以进行使用了。可以看出,类加载是一个将.class字节码文件实例化成Class对象并进行相关初始化的过程。

推荐阅读