前言:相信有一部分java程序员不是太清楚虚拟机是如何将类从java代码变成class文件,再从class文件到到内存,再将我们写的程序转化成具体的程序的,这里就总结下这个过程。
一.类加载的时机
了解java类是如何被加载的之前,我们有必要知道虚拟机是什么时候选择加载java类,下面说下虚拟机规定的几种必须加载java类的场景(有且只有这些场景会加载类)。
1.遇到一些字节码指令时,需要去加载相应得类。
new指令:这个创建对象的指令,所以需要加载对应的类。
getstatic指令:这是获取静态变量值的指令,所以也需要加载对应的类。
putstatic指令:这是为静态变量设置值的指令,所以也需要加载对应的类。
invokestatic指令:这是个专用于调用静态方法的方法调用指令,所以也是需要加载对应的类。
2.使用反射包(java.lang.reflect)中的方法进行反射调用时,如果对应类型没有初始化过则必须先进行初始化.
3.初始化时如果发现父类没有初始化过,则需要先初始化父类。这个操作与super关键字密切相关,与invokespecial指令密切相关(super关键字的底层实现就是invokespecial指令)。
4.虚拟机其多功能时,用户需要指定一个包含main方法的主类,虚拟机必须先初始化这个类。
5.当使用JDK7加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例的最后解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial这四种类型的方法句柄,且这个方法句柄对应的类未被加载过,应先加载对应的类。
6.当一个接口中定义了default方法时(JDK8特性),在接口的实现类发生初始化之前时,应该先初始化该接口。
二.类加载的过程
一个类从java代码到在内存中消失要经历以下几个过程:加载->验证->准备->解析->初始化->使用->卸载。这其中我们最为熟悉的可能就是使用与卸载了,使用自然就是我们的代码被运行起来了。定义的代码跑出了想要的结果,卸载基本可以对应到垃圾收集这个过程,当前类使用完毕,先是该类的对象被回收,最后该类的类型信息也会被回收(被回收的概率很低),那其他五个过程呢,相信很多人都是不太明了,下面介绍下这五个过程。
1.加载
加载的过程:
①通过类的全限定名获取定义此类的二进制字节流,假设这个二进制字节流是class文件,这个第一步就是根据全限定名从本地内存获取到class文件。②将上一步获得的class文件信息加载到运行时数据结构。运行时数据结构包括堆、栈、方法区、等等这些,把加载的class信息存储到这些结构中。③当加载完成class文件后,虚拟机需要为这个类型信息生成一个java.lang.Class对象,作为方法区这个类的数据访问入口,这个对象就是我们在用反射时获取到的某个java对象的Class对象,值得说到的是每个java类被虚拟机加载后,都会有一个对应的Class对象产生,且以后再去生成该类的Class对象其实都是调用原始生成的那一个,这是一个单例模式产生的对象。
二进制字节流的获取方式?
加载过程的第一步就是去获取二进制字节流,上面我们假设这个二进制字节流是class文件,其实二进制字节流还有很多种获取方式。①从class文件中获取,这也是我们比较常用的一种。②也可以从zip、jar、war、ear包中获取,这几种都是我们常用的打包方式(jar<war<ear)。③也可以从网络中获取,如Web Applet。④使用动态代理运行时计算生成。⑤由其他文件生成如JSP。⑥从数据库或者加密的文件中读取等等。等等总之二进制字节流的获取方式很多,只要保证被虚拟机加载时是能被虚拟机识别的class文件就行。
知道了加载的过程,具体是如何加载的呢?
虚拟机其实是使用类加载器来对类进行加载的,往后一点我们会单独介绍类加载这块,现在只需要知道class对象都是被虚拟机中的类加载器加载进虚拟机的。
2.验证
验证阶段顾名思义,就是虚拟机加载完class文件后,来验证这个class文件是否是一个合法正常可执行的文件,如果对于class文件的结构比较清晰,这块还是很好理解的。
验证的过程:
①文件格式验证:校验class的魔数是否是0xCAFEBABE,验证主版本是否是可执行范围之内,因为虚拟机是拒绝执行高于当前版本的class文件的。验证常量池中的常量是否有不背支持的类型。验证的第一阶段主要验证的就是class格式以及文件的正确性。
②元数据验证:验证字节码信息中的类是否符合“java语言规范”,验证这个类是否有父类,除了Object,其他所有类都应该有父类,验证该类是否继承了不能被继承的类,比如final修饰的类是不能继承的。若该类实现了接口、继承了抽象类验证该类是否重写了所有的抽象方法等等。
③字节码验证:前面第一步验证了文件格式,保证是个可以被虚拟机执行的class文件,第二步验证了该类的书写是否规范,这一步就是要验证程序的逻辑是否有问题了,通过数据流控制流分析确定程序语义是合法的、符合逻辑的等。比如:验证操作数栈上的类型与指令都能配合工作。验证方法中的类型转换是否有效等等。
④符合引用验证:经过前面一通的验证,现在class文件已经威胁不到虚拟机的运行,但是仍不能保障可以正常被虚拟机执行,我们还要确定类中的引用是否是这是有效的引用,比如:符号引用中全限定名对应的类是否是真实存在,如果类存在了,还应验证类中是否有需要的方法、字段存在。如果存在了,还需要压制对应的类、方法、字段是否是当前类可以访问的。
3.准备
准备阶段的工作其实比较单一,就是为类变量(static修饰的变量) 赋初始值,数据类型是0,boolean类型是false,引用类型是null。需要注意的是这里只是位类变量赋值,不会为实例变量赋初始值,实例变量的初始值要等到对象创建时,而且虽然这时是位类变量赋值,但是一般不会真正为这个类变量赋实际值,除非该类变量是final修饰的。列出一张各种数据的初始值(类变量与Class对象JDK8之后都在堆中)。
4.解析
解析要干什么?
解析就是把运行时常量池中的符号引用解析成直接引用,在解析阶段将符号引用转化为直接引用也叫静态连接,在运行时再将符号引用转化为直接饮用叫动态连接,那问题来了符号引用是个啥?我们应该知道运行时常量池中存储的信息主要就是符号引用与字面量。符号引用指向的是class文件中或者也有可能是常量池中的一块地址,比如Object obj = new Object();当碰到new指令时,虚拟机会去解析new关键字后面的方法所指向的符号引用,这个引用其实是指向了运行时常量池中的方法表信息,把这个符号引用解析成直接引用就是将该符号引用指向的内存加载进虚拟机执行。构造器对应的方法代码加载进内存,自然就是进入了虚拟机栈中,成为一个栈帧,方法执行完就会创建出一个Object的对象,该对象在堆中,把这个堆中的地址赋给obj,此时obj就成了一个直接引用。
从上面的文字已经可以看出,虚拟机是在碰到了需要解析的符号引用时才发生了解析,那么有哪些情况需要出发解析呢?
《java虚拟机规范》中规定,在执行new、annewarray、multiannewarray、getfield、putfield。。。等17个用于操作符号引用的字节码指令时应该对符号引用进行解析。
下面举三个例子说明下虚拟机是如何把符号引用转换为直接引用的:
①类或接口的解析
假设当前所处的类为D,有一个从未被解析过的符号引用N,他对应的类或接口是C。那么解析这个符号引用N将有以下几步:第一步,如果C不是数据类型,那么会把符号引用N代表的全限定名传递给虚拟机进行加载C类,出现异常的话,解析失败。第二步:如果C是数组,且数组元素是对象,则按照第一种情况加载所属类,然后虚拟机会生成一个代表数组和元素的数组对象。第三步:验证C的访问权限,确认下D是否能够访问到C,可以的话,则转化为直接引用了。
②字段的解析
字段的解析,首先要保证该字段所在的表已经被解析了,字段存在字段表中,字段表中会存储该字段所属类信息,虚拟机会先去解析这个类,假设这个类是C,若C已经解析完成,那么字段解析有如下过程:第一步:若C已经被解析过了,在C中搜索简单名称与字段描述符均相匹配的字段,找到了,则返回该字段的直接引用。第二步:经过第一步没有找到,则去C的接口中去查找,从下而上找,查找简单名称与字段描述符均匹配的字段。找到则返回。第三步:若是第二步仍未找到,则是去其父类中寻找,过程与第二部类似。第四步:如果查到了返回引用过程中会对权限进行验证,未通过或者未查到都会报异常。
③方法的解析
方法的解析,也是首先需要解析该方法所属类,用C表示这个类,若所属类完成了解析。第一步:检查C是否是个接口,是接口直接报错,接口中的方法时不能直接运行的,没有方法体。第二步:通过了第一步的校验,则会在当前类C中查找有无简单名称与方法的描述符都匹配的方法,有则返回直接引用。第三步:若是第二步查找失败,则去查找父类,自下而上。类似于字段的解析。第四步:若是第三步失败了,则去查找接口,自下而上,若是成功了,则报错,因为接口中未实现的方法调用是没有意义的。
以上三个例子都说明了解析这个过程到底要干什么,代码里的引用方开始都是个符号引用,需要将符号引用指向的内容加载进内存,变成直接引用,虚拟机才能使用。
5.初始化
这个过程功能也是比较单一的,就是为了给类级别的信息赋值,前面我们已经说过,在“准备”阶段,会为类变量赋初始值,但是我们自己如果设置了值,并不会在“准备阶段”赋给类变量,这一阶段就是虚拟机收集类级别信息,包含类变量、类代码块啊等等,根据代码为他们复制的过程。
6.总结类加载过程
类的加载过程经过了加载、验证、准备、解析、初始化等这些步骤,才可以正常的被使用,加载主要是将class文件中信息加载进入虚拟机,验证则是对class文件的格式,内容,流程,语法等进行校验,准备阶段是位类变量赋初始值,解析阶段是将符号引用转化为直接引用的过程。初始化则是为类变量赋值的动作。然后我们就可以正常使用类的信息了。