本篇博客主要针对Java虚拟机的类加载机制,虚拟机字节码执行引擎,早期编译优化进行总结,其余部分总结请点击Java虚拟总结上篇 。
一.虚拟机类加载机制
概述
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类加载的时机
类加载的时机不止一种:
- 遇到new等字节码指令时会进行类加载
- 反射调用时会进行类加载
在初始化时,若待初始化的类有父类则其父类先进行初始化(接口除外),并且先初始化包含main的主类。需要注意的是子类引用父类非final静态变量时,只初始化静态变量所在类,即父类,而引用final类型static变量不会引起任何初始化,因为其编译期间就已经储存在常量池中了。另外数组定义也是不会引发类的初始化。比如
Student[] stus=new Student[10];
是不会引起Student类的初始化的。
类加载的过程
加载过程
通过类的全限定名来获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构,在内存中生成一个代表类的数据访问入口的java.lang.Class对象。
验证过程
验证过程的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。主要有
- 文件格式验证:验证魔数,主次版本号,常量类型等。
- 元数据验证:是否有父类,是否继承了不该继承的类,抽象类是否实现了方法等。
- 字节码验证:确保程序语义是合法的,符合逻辑的。如类型转换,跳转指令等。
- 符号引用验证:对类自身以外的信息(常量池中的各种引用)进行匹配校验。
准备过程
正式为类变量分配内存并设置类变量初始值的阶段,只包括类变量而不包括实例变量和final类变量,而且仅仅只是初始化为0值。
解析过程
虚拟机将常量池内的符号引用转换为直接引用的过程。符号引用用一组符号来描述所引用的目标。而直接引用是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。
初始化阶段
在初始化阶段真正开始执行Java程序代码(字节码),执行类的构造器<clinit>()方法,<clinit>()方法是由编译器自动收集所有类变量的赋值动作和静态语句块的语句合并而成,同一类中的静态块与类变量按顺序初始化,在同一个加载器下,一个类只会被初始化一次。
类加载器
实现通过一个类的全限定名获取描述此类的二进制字节流的代码模块称为类加载器。比较两个类是否相等,一定是在同一个类加载器的前提下进行的,否则哪怕Class文件都一样也不相等
类加载器的分类
- 启动类加载器, 负责将存放在
<JAVA_HOME>\lib
目录或-Xbootclasspath
参数指定的路径中的类库加载到内存中。 - 扩展类加载器,负责加载
<JAVA_HOME>\lib\ext
目录或java.ext.dirs
系统变量指定的路径中的所有类库。 - 应用程序类加载器,负责加载用户类路径(
classpath
)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
双亲委派模型
双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException
),子加载器才会尝试自己去加载。
这样做的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。
二.虚拟机字节码执行引擎
虚拟机的执行引擎自行实现,可以自行制定指令集与执行引擎的结构体系。
栈帧
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机栈的栈元素。它储存了方法的局部变量表,操作数栈,动态链接,方法返回地址,对于活动线程来说,只有栈顶的栈帧才是有效的,称为当前栈帧,与其关联的方法叫做当前方法。
局部变量表
局部变量表存放方法参数和方法内部定义的变量。单位是slot(槽),最大可以达到32位。垃圾回收时,slot可以复用,将不使用的变量置为null是有意义的,方便垃圾回收。局部变量不像类变量,是没有初始值的。
JIT编译器
当虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为 “Hot Spot Code”(热点代码),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,完成这项任务的正是 JIT 编译器。
方法返回地址
- 遇到方法的返回指令-->正常完成出口
- 遇到异常并且未处理-->异常完成出口,不会给上层调用者产生任何返回值
方法调用
方法在编译时并不确定方法的真实地址,而是一个符号引用,使得Java的动态扩展能力提升,在类加载过程甚至运行时才确定目标方法的直接引用。
解析
在类的解析阶段将一部分符号引用转换为直接引用,这部分符号引用代表的方法必须“编译期可知,运行时不变”,如静态方法,私有方法,实例构造器,父类方法。final方法也是。
分派
静态分派(与重载相关),依赖静态类型来定位方法执行版本的分派动作。自动转型顺序:char->int->long->float->double->Character->Serializable->Object->char...
动态分派(重写相关),找到操作数栈顶的第一个元素所指向的对象的实际类型,若常量池中的描述符和简单名称都相符,则返回直接引用,否则对其父类进行第二步。
动态分配的实现:
在类的方法区建立一个虚方法表提升效率,若子类未重写父类的方法,则子类的继承方法中地址和父类方法的地址是一样的,若重写了父类的方法,则子类的方法地址就会改变,指向自己实现的版本。如上图Son的clone方法没有被重写,指向的是Object父类的地址,而hardChoice方法被重写了,指向的是Son自己实现的地址。
动态类型语言
类型检查的主题过程在运行期而不是在编译期,如Python,Javascript,Ruby,PHP,与之相对的就是静态语言。
解释执行与编译执行
解释执行为边解释边执行,编译执行则是先将源代码编译成目标语言 (如: 机器语言) 之后通过连接程序连接到生成的目标程序进行执行。
基于栈的字节码解释执行引擎
- 基于栈的指令集:Java编译器输出的指令流
- 基于寄存器的指令集:x86汇编
三.早期编译器优化
编译器
三种编译器:
- 前端编译器:把.java变成.class的过程,eg:Javac
- 后端运行期编译器(JIT):把字节码变成机器码的过程,eg:Hotpot的C1,C2编译器
- 静态提前编译器(AOT):直接把*.java变成机器码的过程,eg:GCJ(GNU Compiler for the Java)
解析与填充符号表
词法分析
标记是编译过程的最小元素,关键字、变量名、字面量、运算符都可以成为标记,词法分析就是将源代码的字符流转变为标记集合。
语法分析
语法分析是根据Token序列构造抽象语法树的过程。抽象语法树是用来描述程序代码语法结构的树形表示方法,每一个节点都代表着程序代码的一个语法结构:包,类型,修饰符等。
注解处理器
类似编译器的一种插件,如果插件对语法树进行了修改,编译器将回到解析及填充符号表的过程重新处理。
语义分析
对语法抽象树进行上下文有关性质的审查,如类型检查。
字节码生成
将前面各个步骤生成的信息转换成字节码写到磁盘中,类构造器<cinit>和实例构造器<init>就是在这个阶段添加到语法树中。
Java语法糖
- 泛型与类型擦除:与C#不一样,Java的泛型是伪泛型,在生成的字节码中已经被替换成了原生类型了,会自动加上类型转换。
- 遍历:自动转换为iterator遍历。
- 装箱与拆箱:==运算在不遇到算数运算的情况下不会自动拆箱。equals方法不会处理数据的类型转换,而==会。
条件编译
编译器不会编译if到达不到的语句,也就是取消分支不成立的代码块,可以查看反编译后的代码验证条件编译。