概述
编译器是一段“不确定”的操作过程
编译器类型
- 前端编译器:将Java代码编译为class字节码
- 代表:
sun公司的javac(Java语言编写)、Eclipse JDT中的增量编译器ECJ
- 后端编译器(JIT编译器):将字节码转变为机器码
- 代表:
HotSpot VM的C1、C2编译器
- 静态提前编译器(AOT编译器 Ahead of Time Compiler):将Java代码编译为机器码
-代表:GNU Compiler for the java、Excelsior JET
1. 早期(编译期)优化
主要说明Sun Javac的大概编译过程
- 解析与填充符号表过程
1.1 解析:
1.1.1 词法分析:将源代码的字符流转为标记(Token)集合(例如:关键字,变量名,运算符,字面量)
1.1.2 语法分析:根据Token序列构造抽象语法树,语法树的每一个节点都是一个语法结构(例如:包,类型,修饰符,运算符,接口,返回值,代码注释)
1.2 符号表填充:符号表由符号地址和符号信息组成的表格(可以看成K-V键值对) - 注解处理
插入式注解处理器的标准API在编译期间对注解进行处理 - 分析与生成字节码
因为由于由语法分析所生成的抽象语法树不能保证逻辑性,故而语义分析是对正确性的审查
int a =1;
boolean b = false;
int c = a+b;//编译不能通过
- 3.1 标注检查
主要检查:变量使用前是否已经被声明、变量与赋值之间的数据类型是否能匹配 - 3.2 数据及控制流分析
主要检查:对程序的上下文更进一步的验证,如:局部变量在使用前是否已经赋值、方法的每条路径是否都有返回值、是否所有的受检异常都被正确的处理等问题
//这里的两个方法,编译出的字节码没有一点区别,只是有final修饰的不能被改变
public void test(final int a) {}
public void test(int a) {}
- 3.3 解语法糖
泛型擦除(实际上就是将结果强转)、变长参数、自动装箱/拆箱、内部类、枚举类、断言语句、对枚举和字符串的支持、try语句定义和关闭资源等
条件编译:使用条件为常量的if语句
//1.
public static void main (String[] args) {
if (true) {
System.out.println("1");
} else {
System.out.println("2");
}
}
//在编译之后的结果
public static void main (String[] args) {
System.out.println("1");
}
//2. 下面语句将会拒绝编译
public static void main(String[] args) {
while (false) {
System.out.println("错误");
}
}
- 字节码生成
字节码生成不仅仅将前面步骤生成的信息转换为字节码写到磁盘中,还要进行少量的代码添加和转换工作
2. 晚期(运行期)优化
部分商用虚拟机(Sun HotSpot、IBM J9)中,Java程序最初通过解释器(interpreter)解释执行,当虚拟机发现某个方法或者代码运行特别频繁时,就会把这些代码定义为“热点代码”。为了提高热点代码的执行效率,在运行时,虚拟机会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化。完成这个任务的编译器就是即时编译器(JIT just int time complier)
JRockit没有解释器,因此启动时间较长
- 为何HotSpot 虚拟机要使用解释器与编译器并存的架构?
两种编译器各有优势: 解释器:启动快、执行快; 编译器:执行效率高
Client模式启动速度较快,Server模式启动较慢,但是启动进入稳定期长期运行之后Server模式的程序运行速度比Client要快很多 - 为何HotSpot 虚拟机要实现两个不同的即使编译器?
client compiler:获取更快的编译速度;server compiler:获取更好的编译质量。具体使用哪种,虚拟机会根据自身版本和宿主机的硬件性能自由选择,当然也可以强制运行某种模式,无论编译器采用client compiler 还是server compiler,解释器与编译器搭配使用都称为“混合模式(mixed mode)”,也可以强制虚拟机运行编译模式(-Xcomp)或者解释模式(-Xint)
MacBook-Pro:~ shizhenshuang$ java -version
java version "1.8.0_231"
Java(TM) SE Runtime Environment (build 1.8.0_231-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.231-b11, mixed mode)
MacBook-Pro:~ shizhenshuang$ java -Xint -version
java version "1.8.0_231"
Java(TM) SE Runtime Environment (build 1.8.0_231-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.231-b11, interpreted mode)
MacBook-Pro:~ shizhenshuang$ java -Xcomp -version
java version "1.8.0_231"
Java(TM) SE Runtime Environment (build 1.8.0_231-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.231-b11, compiled mode)
- 程序什么时候用解释器执行?什么时候用编译器执行?
当程序刚开始启动的时候,解释器优先执行,省去编译时间,当程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码。那么,什么时候编译器开始执行呢?
编译对象(热点代码)与触发条件
- 被多次调用的方法
- 被多次调用的循环体(实际编译的事整个方法)
方法替换使用的是栈上替换(On Stack Replacement)
判断一段代码是不是热点代码,是否需要触发即时编译,这样的行为成为热点探测。热点探测目前流行的有两种方法
- 基于采样的热点探测
虚拟机周期性检查各个线程的栈顶,如果出现某个(或某些)方法经常出现在栈顶,那就是热点方法 - 基于计数器的热点探测
虚拟机为某个方法或者代码块建立计数器,统计执行次数,如果执行次数超过一定的阈值就认为它是热点代码。(阈值:client模式是1500,server模式是10000,可以通过参数:-XX:CompileThreshold 设置)
计数器又分为:
2.1 方法调用计数器
2.2 回边计数器
- 如何从外部观察即时编译器编译过程和编译结果?
使用debug,fastdebug版本的虚拟机(JDK6u25之后就不提供下载了),运行时,添加参数-XX:+PrintCompilation
- 编译优化项
代表
- 1 方法内联:1. 除去方法调用的成本;2. 为其他优化建立良好的基础
class A {
int age;
public int getAge() {
return age;
}
}
public static void main(String[] args) {
A a = new A();
int y = a.getAge();
}
//优化后的代码如下(用Java代码表示)
public static void main(String[] args) {
A a = new A();
int y = a.age;
}
- 2 消除冗余代码
- 3 代码复写传播
int y = 2;
z = y;
int sum = y+ z;
//复写传播
y=y;
int sum = y+y;
//消除冗余代码后
int sum = y+y;
典型代表
1. 公共子表达式消除:如果一个表达式E已经计算过了,并且从先前的计算到现在E中的所有变量的值都没有改变,那么E的这次出现就成为了公共子表达式,也就没必要再花时间对他进行计算了(若a+b=c, 则int i = a+b+1 --> int i = c+1)
2. 数组范围检查消除:1. 编译时检查,2. 通过数据流分析
3. 方法内联:就是把目标方法的代码拷贝到调用方,避免发生真实的方法调用
4. 逃逸分析(JDK1.6开始):它并不是直接优化代码的手段,而是为其他优化手段提供依据的分析技术。逃逸分析的基本行为就是分析对象动态作用域。当一个对象在方法中被定义,通过参数的形式传递到其他方法中,称为方法逃逸。甚至有可能被外部线程访问到,譬如赋值给类变量或在其他线程中访问实例变量,称为线程逃逸。如果能够确定一个对象不会逃逸到方法或者线程之外,则可以为这个对象做一些高效的优化
4.1 栈上分配:将对象分配在栈上,内存空间随着栈帧出栈而销毁,减少gc回收的压力
4.2 同步消除:如果确定一个对象不会逃逸出线程,就无须对这个对象实施通过的措施(线程同步是一个相对耗时的过程)
4.3 标量替换:标量是指不能再拆解的数据类型,如原始数据类型。如果一个数据还能被分解,它就称为聚合量,如对象。标量替换就是将对象的成员变量替换为原始的数据类型。逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆解,那么程序执行的时候可能就不会真正的创建这个对象,而是创建它的若干个被这个方法访问的成员变量
- Java与C++的编译器对比
即:即时编译器与静态编译器的对比
- 即时编译器占用的是用户运行的时间,具有很大的时间压力。而编译时间成本在静态编译器中并不是主要关注点
- Java语言是动态类型安全语言。这就意味着由虚拟机来确保程序不会违反语义和非结构化内存,这就使得虚拟机得频繁的动态检查空指针,数组下标范围,类型转换等
- Java中虽然没有virtual关键字,但是接受者进行动态选择的频率要远远大于C/C++语言,优化难度要大于静态编译器
- Java语言是动态扩展语言,运行时加载新的类可能会改变程序类型的继承关系
- Java语言中的对象都是在堆上分配,只有方法中的局部变量才在栈上分配。而C/C++则有多种内存分配,既可以在堆上,也可以在栈上。C/C++主要是用户程序代码回收内存分配,因此效率上要高于Java