7.1 概述
不同于传统的语言,java在编译的时候不需要连接,类型的连接、初始化的过程是在运行期间完成的,符号的入口是保留在常量池里的,这给java提供了很大的灵活性,支持动态扩展。比如一个面向接口的程序,可以在运行的时候在指定接口的实现类。
7.2 类加载时机
虽然java在编译的时候不需要连接,但一个java程序要想跑起来肯定是要连接的,这个连接的过程就在class文件运行的时候执行。
除了使用和卸载,class文件被加载的过程分为5个阶段。但这5个阶段并不是像图上这样按部就班的执行的,具体的有两个特点:1、解析的位置不一定。有可能在初始化前,也有可能在初始化后面。一个类被初始化完成之后才开始解析,这就是传说中的动态绑定,这就是多态实现的途径。 2、各个阶段是按部就班的执行,但并不是一个过程执行完毕后才开始下一个,而是一个过程在执行的时候就开始执行下一个过程。
虚拟机的规范并没有规定什么时候开始加载一个类,而是规定什么时候初始化一个类,那么在初始化之前的过程一定在初始化之前执行。虽然规范中规定初始化一个类的时机有很多,但大体上都可以不严禁的表述为第一次使用一个类的时候。
7.3 类加载的过程
7.3.1 加载
加载是类加载的第一个步骤,名字很像,load是class load的一个过程。在这个过程中虚拟机规范要求虚拟机的具体实现要完成以下三个动作:1、通过类的全限定名获取类的二进制字节流2、把二进制字节流从存储的数据结构转换为在方法区的运行时数据结构 3、在内存中生成一个类对应的java.lang.Class对象作为类的访问入口。
首先第一步,规范只要求了要通过全限定名来加载一个类的二进制字节流,但是没有要求从何处获取。最基础的如果从ZIP包里获取就是我们导的JAR包;运行时候生成就是JDK的动态代理实现即用动态代理为一个接口生成一个实现类;如果是由其他文件生成就是jsp。
其次是最后一步,规范只要求了要在内存里生成一个对象作为类文件的访问入口,但是没有要求在内存的哪个区域放这个对象,Hotspot虚拟机是在方法去里放的这个对象,即一个对象放在了方法区里。
加载和后续的连接过程是交替实现的,可能一个类没有被完全加载但是已经开始连接了。但是他们开始的先后顺序的固定的,即必须先执行加载,这是肯定的不加载去连接什么?
7.3.2 验证
需要验证的事情又两件:1、安全性 2、是否符合版本。class文件是否符合版本很好理解,前面提到过class文件开始的四个字节就包括魔数和代表版本的字节,只要简单的比较一下就可以了。安全性主要是针对一个class文件是否出现了非法的操作,比如数组越界、使用一个未实现的类。虽然如果你出现了数组越界之类的错误编译器会拒绝编译,但是你可以直接自己写一个class文件来实现上述的功能,所以在这一步虚拟机需要检查class文件是否是安全的。
- 文件格式验证。验证加载进来的二进制字节流能不能被转换成方法区要求的数据结构并被放倒方法区里
- 元数据验证。验证是否符合语言规范,主要是验证该类与其父类是否存在冲突
- 字节码验证。主要验证方法是否存在问题
- 符号引用验证。什么是符号引用呢?A类里面引用了一个B类的方法or属性,那么他就是属性,我在编译的时候哪怕引用了一个不存在的B类,我编译也是可以通过的,但是在class加载的时候,我要使用A类了,A类还引用了一个不存在的B类,那肯定是不可以的。这里的验证主要是验证引用是否符合规范,即访问权限够不够,是否存在该属性。
7.3.3 准备
这一步才开始在方法区里为一个类分配内存并为其中的一些变量设置初始值。
首先会被分配内存的变量只有static变量,即静态的成员变量。
其次会为所有的值设置一个初始值即0 false这些,即使你声明了一个static int a= 6,也不会在这一步执行初始化把a设置成6,a的设置需要再执行cinit函数的时候才会被设置。但是有一种情况下会分配内存并直接初始化,final static int a =6,这是一种编译优化机制,被final修饰的静态类变量会在准备阶段直接完成赋值。
7.3.4 解析
解析就是符号引用转换成直接引用的过程。符号引用这是一个充满编译原理味道的名字,在连接中,不属于自己的方法名和属性名都是符号。直接引用很好理解,就是一个真正的引用,可以不严谨的理解为指针。所以整个解析的过程就是解决java在编译的时候没有连接这个问题的,编译的时候可以不连接,但是运行起来了肯定需要连接,我调用了A.b方法,我就得知道A.b方法的具体的地址,这就是解析过程完成的工作,把一个符号转换为具体的内存地址(姑且这么理解).
正如前面所说,解析的过程可以发生在初始化前或者后面。java虚拟机没有规定解析的具体的时间,java里一些字节码的操作数是符号引用,如new指令,java规定在执行new指令之前必须完成解析这一过程。即虚拟机规范规定的是在执行操作数为符号引用的指令前必须完成解析的过程。
7.3.5 初始化
前面的阶段用户的参与性是很少的,几乎都是虚拟机来执行的,用户static字段的设置在这一步被体现出来。在准备阶段即给static属性分配内存的时候已经为其全部赋值为0等默认值(final)修饰的除外。在初始化阶段会执行<cinit>方法去初始化变量。<cinit>方法是编译器在一个类被加载的时候执行的方法,编译器会收集所有对staic属性的赋值动作和static语句块里的动作并生成一个<cinit>函数去初始化一个类,言下之意如果一个类没有相关的赋值语句就可以不生成赋值语句。
还有一个和<cinit()>名称很像的方法,<init()>方法,<init()>用来新建一个对象并且要求在执行<init()>之前要先执行父类的<init()>方法,即会显示的调用父类的构造器。但是初始化一个类的时候并不会显式的调用父类的<cinit()>方法,虚拟机会保证父类的先执行。
多线程下回保证<cinit()>执行的正确性。
接口中虽然不能使用静态语句块,但是也有对static变量初始化的赋值操作,但是接口的<clinit>方法执行的时候不会保证父接口的<clinit>方法先执行,只有使用父接口的<clinit>方法的时候才会执行父接口的clinit。