Java源码被编译成class字节流后,需要把class中的信息加载到内存中才能使用,我们就来讲一讲这个过程中所发生的事情;
Java与C不同,Java源码经编译后的class文件作为一个单元文件存放,直到运行期间才动态解析、动态链接;
首先class遇到什么情况会被加载呢?
虚拟机规范严格的定义了5中情况必须对类进行初始化(加载、验证、解析等需要在此前完成,具体有多“前“是虚拟机的具体实现,因此这里也提供了灵活性)
- 调用new, getstatic, putstatic, invokestatic的时候
- 使用java.lang.reflect包进行发射操作的时候
- 初始化一个类发现其父类还没初始化的情况下,需要先对其父类初始化
- 虚拟机启动时包含main()方法的类,虚拟机会主动使用这个类
- JDK1.7以上版本,对动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic, REF_putStatic, REF_invokeStatic的方法句柄,并且这个方法所对应的类还没初始化,则需要对其初始化
虚拟机规范在这里用——有且仅有——来描述这五种场景,并称为主动引用一个类,其他情况下称为被动引用,并且被动引用不会触发一个类的初始化操作
考虑一些情况:
- A类中的静态常量M,被B类引用,则不一定会触发类的初始化,因为在编译器期的传播优化已经将M的值存储到了B类的常量池中
- 对实例字段进行操作,不会触发。因为对实例字段操作,操作结果是堆或者方法区上(强调方法区是因为有的虚拟机实现,例如HotSpot虚拟机对于类成员对象的分配就在方法区)的对应实例的那块内存操作,好吗?
对于接口来说——接口也有初始化过程,接口与类的区别在于第三条,接口初始化时不要求其父类接口必须初始化,原因很简单,接口没事实质性内容,对于对象的构建过程也无太大帮助,不像父类,父类的实例字段或方法是对象的一部分,构建对象时,先要对对象构建对象父类中的那部分。
该过程分为:加载,连接(验证,准备,解析),初始化
1. 加载
就是把class字节码的内容先加载到内容,要不然怎么用呢,注意这里强调class字节流,而不是class文件,因为class字节流的来源可能是硬盘,网络,JAR包,加密后的文件,数据库甚至是现场生成的,囧!
当然加载过程中还会干其他的事情,一般来说是这样:
- 首先通过一个全限定的名称来获取此类的二进制class字节流
- 把这个字节流的格式转换成运行时所需要的格式,废话,class格式设计的那么紧凑是为了传输和保存,不变换格式就在内存直接用,效率是不是太低呢?另外这里加载的位置应该没有争议,就是方法区
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各个数据的访问入口,这个很明显,java支持反射(自省),所以必然需要这么做
另外这里比较特殊的是数组类,首先说数组类是什么,Java中的数组可不像C语言那么低级,直接就是一块连续的内存区域,每个类型的Java数组都是一个由内存中的一个类表示好吗,意思就是int[]与String[]在方法去都有对应的类,当然这些数组类是有一些共性的。上面说到数组类,这个数组类是虚拟机自己生成,目的是为了运行期间对数据的操作。有人问问什么要这个东西啊,因为上面说了Java数组可不像C那么低级,Java数组可是引用类型啊,引用类型就的对象就必然要有对应的类。
这里还要说明一个概念,要唯一区别一个类型需要:类加载器+类的全限定名才行,那么数组类是由虚拟机生成的没有对应的加载器怎么办?对啊,使用数据元素类型的类加载器就好咯,你问我如果元素是8中基本类型的一种,不需要加载怎么办,那就只好用引导类加载器来关联喽,这里不知道类加载器没关系,后面会讲到
加载过程结束,这时候class字节流中的数据已经按照虚拟机需要的格式存储在方法区了。
2.连接——验证
验证是连接的第一步,目的是为了保证class字节流的内容无误(不会危害虚拟机)
其实Java源码在编译时候就会保证语法规则的正确性,但是class字节流是可以跳过Java源码直接编译的,因此难免有错误
验证阶段非常重要,它确保了Java的安全性(其实是class文件的安全性,因为以后基于class文件的语言都可以共享这一点了)
如果验证到不符合class文件的约束虚拟机会抛出一个java.lang.VerifyError的异常或其子类异常
验证阶段大致分为:文件格式验证,元数据验证,字节码验证,符号引用验证
- 文件格式验证:例如魔数:0XCAFEBABE是否正确,版本是能够被加载,常量池的常量是否有不符合规定的(tag标志),指向常量的索引是否正确,CONSTANT_Utf8_info型的常量是否有不被支持的编码,class文件本身是否完整,注意只有这一验证阶段是基于字节码的,以后的阶段都是基于方法区的被转化后的格式的,因此这一验证过程实质发生在加载阶段
- 元数据验证:这个阶段是对字节码描述的信息进行语义分析,例如是否有父类,是够继承了final类,若父类是抽象类是否实现了其所有方法等等。
- 字节码验证:这个阶段最为复杂,它要保证该类在运行期间不会做出危害虚拟机的事情,例如验证任意时刻操作数栈的数据类型都能够与操作码配合,不会出现诸如iadd操作引用类型的情况,保证跳转指令不会跳出方法体,保证方法体内的类型转换有效等。值得说明的是:这个阶段的验证只是力求减少这类错误,而不能完全根除,详见著名的停机问题。在JDK1.6以前这个阶段是由类型推导方式完成的,而1.6以后提供了StackMapTable属性表,即在编译器已经按照方法体的基本块进行划分,并对本地变量表和操作数栈的对应状态进行记录,因此在验证阶段只需要类型检查就可以,1.7以后已经强制使用后者
- 符号引用验证阶段这个阶段的校验发生在虚拟机将符号引用转化为直接引用的过程中,如:符号引用能否通过全限定名找到对应的类,在指定类中是否存在符合方法或字段描述符和简单名称的方法和字段,符号引用对应类,字段或方法的访问性等等。
其实验证阶段不一定存在,但正是由于验证的存在大大增加了Java的安全性。
3. 连接——准备阶段:
准备阶段比较简单,就是为静态变量分配初值(初始化内存),引用类型是null,基本类型一律为0,但是,如果该静态变量有ContantValue这个属性表(其实就是指常量,但仅限于基本类型和String类型,没办法其他引用类型class字节码也无力描述),那么就按照此值分配。
4. 连接——解析阶段
解析就是将常量池中的符号引用替换成直接引用的过程,注意Java是明确定义了符号引用的格式的例如:java/lang/String
直接引用,可以是指针,偏移量,句柄等,看虚拟机的实现
虚拟机规范并未规定解析发生的时间,只是规定在anewarry, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual, ldc, ldc_w, multianewarray, new, putfield和putstatic这16个指令前解析发生就可以。
对一个符号引用进行多次解析是很常用的事情,比如类A,B中引用类C则A,B都需要对C的符号引用解析,因此可在常量池做一些缓存