Java虚拟机 系列文章
Java虚拟机1 内存管理、GC,包括 Shenandoah ZGC
Java虚拟机2 G1垃圾回收详解, 参数, 日志
Java虚拟机3 Class文件及类加载
Java虚拟机4 方法调用原理、动态类型支持
Java虚拟机5 编译与优化 (本文)
Java虚拟机6 内存模型、线程、锁
总结 Java 不支持的语法特性
Java 协程:Loom Project 实战
其他JVM语言
Java编译
- 前端编译器:JDK的Javac,把java文件编译成class文件
- JIT(Just In Time) 即时编译器:HotSpot的C1、C2、Graal编译器,运行期把字节码转变成本地机器码
- AOT(Ahead Of Time) 提前编译器:Jaotc、GCJ等,直接把程序编译成目标机器二进制代码,不常用
前端编译
编译过程:
- 初始化插入式注解处理器 (通过 javac -processor 指定)。
- 解析与填充符号表,包括:
词法、语法分析。将源代码的字符流转变为标记集合,构造出抽象语法树。
填充符号表。产生符号地址和符号信息。 - 插入式注解处理器的注解处理过程。
- 分析与字节码生成,包括:
标注检查。对语法的静态信息进行检查。
数据流及控制流分析。对程序动态运行过程进行检查。
解语法糖。将简化代码编写的语法糖还原为原有的形式。
字节码生成。将前面各个步骤所生成的信息转化成字节码。
前端编译几乎不做性能优化,原因是Java虚拟机上有很多语言,而前端编译的优化只对单个语言生效。
即时编译(JIT)
Java程序首先解释执行,当某些代码块运行特别频繁时,就会把这些代码认定为“热点代码”,并将其编译成本地机器码,并进行优化。
HotSpot虚拟机有三个即时编译器:
- C1:客户端编译器,可通过 -client 指定
- C2:服务断编译器,可通过 -server 指定
- Graal:JDK10新加入,目标是替代C2
这种解释器和编译器搭配使用的模式称为混合模式(Mixed Mode)。
参数 -Xint:强制虚拟机运行于解释模式(Interpreted Mode),这时全部代码都使用解释方式执行。
参数 -Xcomp:强制虚拟机运行于编译模式(Compiled Mode),这时候将优先采用编译方式执行程序。
-version 参数可以显示运行模式,如下所示:
java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)
1.7之后server模式默认开启分层编译。
分层编译:
- 第0层。程序纯解释执行,并且解释器不开启性能监控功能(Profiling)。
- 第1层。使用客户端编译器将字节码编译为本地代码来运行,进行简单可靠的稳定优化,不开启性能监控功能。
- 第2层。仍然使用客户端编译器执行,仅开启方法及回边次数统计等有限的性能监控功能。
- 第3层。仍然使用客户端编译器执行,开启全部性能监控,除了第2层的统计信息外,还会收集如分支跳转、虚方法调用版本等全部的统计信息。
- 第4层。使用服务端编译器将字节码编译为本地代码,相比起客户端编译器,服务端编译器会启用更多编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的激进优化。
提前编译
- 1998年,GCJ (GNU Compiler for Java)
- 2013年,ART(Android Runtime)
- 2017年,Java9 jaotc
提前编译可以做耗时很高的全程序分析。
即时编译的优势:
- 性能分析制导优化
比如某个程序点抽象类通常是什么实际类型、条件判断通常会走哪条分支。 - 激进预测性优化
做一些有可能出错的优化,出错后退回到低级编译器甚至解释器。 - 链接时优化
可以实现跨链接库的优化
编译优化
优化过程举例
原始代码:
class B {
int value;
int getValue() {
return value;
}
}
void foo() {
int y = b.get();
// do something
int z = b.get();
int sum = y + z;
}
第一步:方法内联
void foo() {
int y = b.value;
// do something
int z = b.value;
int sum = y + z;
}
第二部:冗余访问消除
void foo() {
int y = b.value;
// do something
int z = y;
int sum = y + z;
}
第三部:复写传播
void foo() {
int y = b.value;
// do something
y = y;
int sum = y + y;
}
第四部:无用代码消除
void foo() {
int y = b.value;
// do something
int sum = y + y;
}
方法内联
消除方法调用成本,并且是其他很多优化的前提。
非虚方法可以直接内联。
通过类型继承关系分析,对只有一个版本的虚方法进行内联。称为守护内联。
加载新类后,守护内联可能失效,需要回退编译结果。
有多个版本的虚方法,使用内联缓存。
逃逸分析
基本原理:在一个方法里创建对象后,该对象可能被其他方法访问,称为方法逃逸;也可能被其他线程访问,称为线程逃逸。对不逃逸的对象可进行一些优化:
- 栈上分配
对象分配到栈上,支持不逃逸或方法逃逸。 - 标量替换
不去创建对象,而是直接创建它的被这个方法使用的成员变量来代替。仅支持不逃逸。 - 同步消除
如果一个变量不会逃逸出线程,这个变量的同步措施就可以消除。
公共子表达式消除
如果一个表达式之前已经被计算过了,并且表达式中所有变量都没有发生变化,则不需要重新进行计算。
数组边界检查消除
比如在循环中进行数组访问,如果分析出下标取值范围不越界,则可以消除上下界检查。