1 Java源码编译机制
java虚拟机中执行的是class文件,因此先熟悉JDK是如何将Java代码编译成class文件是很必要的。
图1是利用javac将java代码编译为class文件的步骤
1.1 分析和输入到符号表
分析过程包括词法分析与语法分析,词法分析的过程是将代码字符串转化为token序列,语法分析的过程是将token序列生成抽象语法树。
这里以一个例子来说明语法树,语法树是源代码语法结构的一种树形表示方式,如下所示代码:
public class Person {
private String name;
private int number;
public Person(String name, int number) {
this.name = name;
this.number = number;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
}
其语法树大概如下图所示:
对其中一些符号的解释:
Compilation:Person.java文件
TypeDeclaration:Person类
FiledDeclaration: “private String name;”,"private int number;"语句
MethodDeclaration:表示Person类所有方法的声明和实现语句。
输入过程需要将当前类的父类、接口等信息输入到自身符号表中。
1.2 注解处理
该过程主要处理用户自定义的注解
1.3 语义分析和Class文件的生成
语义分析主要依靠之前第一步时生成的抽象语法树,主要分析的内容有变量使用前是否已经声明、语句是否都可到达等。
Class文件的生成
生成步骤:
- 将实例成员构造器收集到该类构造器中
- 将静态成员构造器收集为
- 利用后续遍历将抽象语法树生成为字节码
- 将符号表输出为class文件
Class文件的内容
- 结构信息:版本号等
- 元数据:全限定类名、方法名和属性、字段名和属性等常量池里的东西
- 方法信息:字节码等
2 类加载机制
类加载机制是指将class文件加载到java虚拟机并形成Class对象的机制。
其过程可以划分为三个步骤:装载、链接、初始化。
2.1 装载
- 通过全限定类名获取其二进制字节流。
- 将字节流代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成代表这个类的Class对象,作为方法区这个类的各种数据的访问入口。
2.2 链接
链接过程包括验证-准备-解析
- 验证:校验二进制字节码的格式是否正确。
- 准备:隐式初始化的过程
- 解析:将符号引用替换为直接引用
2.3 初始化
以下四种情况会触发初始化过程:
- 调用了new
- 反射调用了类中的方法
- 子类调用了初始化
- JVM启动过程中指定的初始化类
初始化时会进行显示赋值。
3 类执行机制
3.1 字节码解释执行
方法的调用有以下四个指令:
- invokestatic:调用静态方法
- invokeinterface:调用接口方法
- invokespecial:调用构造方法
- invokevirtual:调用除上面三种外的方法
以下面这个简单的程序为例看一下字节码的执行:
public int solve(int a,int b){
int c = 2;
int d = c*(a+b);
return d;
}
在先使用javac编译过后,使用javap -c ClassName
public int solve(int, int);
Code:
0: iconst_2//将int类型的常量2放入操作数栈
1: istore_3//将操作数栈栈顶值弹出放入局部变量区第三个变量
2: iload_3//装载局部变量区第三个变量到操作数栈
3: iload_1//装载局部变量区第一个变量到操作数栈
4: iload_2//装载局部变量区第二个变量到操作数栈
5: iadd//将操作数栈上面两个数(需要是int类型)弹出做加法后得到的值再放入操作数栈
6: imul //将操作数栈上面两个数(需要是int类型)弹出做乘法后得到的值再放入操作数栈
7: istore 4//将操作数栈栈顶值弹出放入局部变量区第四个变量
9: iload 4//将局部变量区第四个变量压入操作数栈
11: ireturn //返回操作数栈顶值
3.2 编译执行
解释执行的效率不如编译执行,因此为提升代码的执行性能,通常使用JIT编译器对执行频率较高的代码进行编译,对不频繁的代码继续使用解释的方式。编译的方法Sun JDK提供了两种模式:client compiler和server compiler.
3.2.1 client compiler
client compiler又称为C1,较为轻量级,只做少量性能开销比高的优化。其优化方式主要有方法内联、去虚拟化、冗余消除。
- 方法内联:把调用的方法的指令直接植入当前方法中。
- 去虚拟化:当调用的方法只存在一个实现类,其也可以用方法内联。
- 冗余消除:根据运行时状况进行代码折叠或者删除
3.2.2 server compiler
server compiler又称C2,较为重量级,其中逃逸分析是其很多优化的基础。
- 逃逸分析:一种确定指针动态范围的方法。
java中判断一个对象是否逃逸主要依据以下两点:
- 对象是否被赋值给堆中对象的字段或类的静态变量。
- 对象被传进了不确定的代码中去运行。
基于逃逸分析可以做标量替换、栈上分配、同步消除。
- 标量替换:在对象未逃逸时,可用标量代替聚合量。
- 栈上分配:在对象未逃逸时,可在栈上分配内存。
Person person = new Person(1, 2);
System.out.println("person.weight=" + person.weight + ";person.high=" + person.high);
因为person未逃逸,那么其可以分配在栈上;同时也可以使用标量替换的方式转化为下面这种形式:
int high = 1;
int weight = 2;
System.out.println("person.weight=" + weight + ";person.high=" + high);