在现代的编译原理中,程序从源代码转换为可执行指令的步骤都是首先进行词法分析,生成我们所说的Token序列,也就是单词流,再对单词流进行语法分析得到语法树,经过优化生成中间代码后,再生成目标代码。在C++中,这一整套步骤可以由一个完整的编译器来实现。JAVA中就不一样,大家都知道编译器有前后端的概念,在JAVA中编译器的前后端就是分离开的。JAVA程序经过javac编译器编译后,会生成.class格式的文件,这个文件里包含的内容全是二进制字节流,也就是通常所说的JAVA字节码,javac编译器就是JAVA编译器的前端。
JAVA字节码生成后,经过解释器就可以执行,但解释性的语言速度都是比较慢的。后来发展出即时编译器,在虚拟机发现某段程序调用特别频繁时把它们标注为“热点代码”,然后用即时编译器把这段热点代码变成机器码。即时编译器的英文是(JustInTimeCompiler),也就是JIT编译器。解释器节省编译的时间,让程序立即执行,JIT可以随着程序的执行将越来越多的字节码变成机器码,使得运行中程序的效率越来越高,二者相互配合。上面这么多概念看起来和类加载机制并没有什么关系,但对于理解类加载机制,肯定要先理解javac和JIT编译器是什么分别起什么作用。
JAVA的类加载,当然发生在.class文件生成后,是将.class文件读入内存的过程。经过一系列的加载和解析操作最终能让虚拟机直接使用这个类,类加载机制就完成了它的所有步骤。下面给出一张很著名的图,一个JAVA类的生命周期如下:
其中,验证、准备、解析三个阶段属于连接阶段,解析阶段虽然放在第四位,但实际上的发生顺序是不确定的。JAVA虚拟机规范规定一个类被主动引用的时候,必须进行初始化。这里的主动引用指的用new实例化、调用某个类的静态方法、读取非final静态字段、通过反射调用(例如Class.forName)等等。另外,某个类触发初始化的时候,它的父类如果没初始化也会被初始化。
所谓类的加载过程,就是上图中除了使用和卸载之外的另外五个过程。下面就来逐一介绍:
先来说加载的过程,这个过程把类的字节流转换为虚拟机所需的格式,存到方法区中去。当加载完成后,将会生成一个class对象,这个class对象可以作为入口来访问之前存进方法区的那些类型相关数据。这个class对象是java.lang.class的对象,因为是一个对象,所以其自身是存在堆区的。每个JAVA类(包括八大基本类型在内)都有一个这样的对象以记录自己的类型信息存在哪。所以说一旦某个类完成了加载,虚拟机会用它的class对象来创建这个类的所有实例。可以对实例调用getClass()方法或调用Class.forName(“类名”)方法来获取某个类的class对象。
补充一个class对象的用途,我们可以通过反射机制来用class对象创建一个类的实例:
Class classType = Class.forName("类名");
Object obj = classType.newInstance();
类名 instancs=(类名)obj;
扯得有点远。加载过程完成之后就开始验证(这里并不是说完全加载完,实际上加载和验证可以交叉进行,但是验证的部分一定是已经被加载的)。验证就是对加载的字节流进行检查,确保没有危害虚拟机的恶意代码包含其中。为什么要验证呢,因为.class文件可以从磁盘、从网络、从内存等等地方获取,并不能确保内容的安全。当然如果用户知道自己的.class文件来源是完全安全的,可以关闭这个阶段,不进行验证。对应命令是-Xveryfy:none。
然后就到了准备阶段。这个阶段在方法区为类变量分配内存。既然是在方法区分配内存,那猜也知道这里的类变量就是指静态变量了(无final修饰)。分配完成后类变量的值会被初始化为零——初始化阶段才会将变量的值赋进去。 这个“零”对于各种数(int,double等)就是指0,对于boolean指false,对于char指\u0000,对于引用则是null。
接下来介绍最重要的解析过程。这个阶段将常量池的符号引用解析为直接引用。对上一句话我用三段话来避免混淆。其一是常量池,这里的常量池指的是.class文件的常量池,是这个二进制字节流文件中的一个区域,不是字符串池或运行时常量池;其二,符号引用是描述其所引用的目标的字面量,这些信息就存储在.class文件的常量池里,包括七个类型;其三,直接引用并不一定是指针,只有是能够定位到目标的句柄就可以。如果是类或接口的解析,解析时符号引用会被传给所在类的类加载器,类加载器会去加载符号引用对应的类。如果是字段的解析,那就会先解析字段所属的类或接口。方法的解析与字段的解析类似,当前类下没有相匹配方法时,会递归向父类、实现的接口寻找。
实际上常量池解析的过程非常复杂,想要彻底理解只看上面一点介绍肯定不够,需要熟悉.class文件的格式才能知道每一步是怎么进行的,这里我就不展开说了,因为担心会说不清。
最后来说说初始化,这个过程完成后,类加载就算是完成了。初始化阶段会执行到我们编写的java代码,包括所有的类变量赋值语句和静态代码块。这些代码会被收集到