程序员对效率的追求,是永无停止的。
学习资料主要参考: 《深入理解Java虚拟机:JVM高级特性与最佳实践(第二版)》,作者:周志明
早期(编译期)优化
1. 概述
Java的编译期,有很多意思:
- 可以是指前段编译器把 *.java 文件转为 *.class 文件的过程:Eclipse、Javac
- 可以是指虚拟机的后端运行期编译器(JIT编译器, just In time Compiler)把字节码转为机器码的过程:HotSpot VM的C1、C2编译器
- 也可能是指使用静态提前编译器(AOT,Ahead Of Time Compiler)直接把 *.java 文件编译成本地机器代码的过程:GNU Compiler for the java、excelsior JET
本章主要讨论第一类。
2. Javac 编译器
1)整体过程
从Sun Javac 的代码来看,编译过程大致可以分为3个过程:
- 解析与填充符号表过程
- 插入式注解处理器的注解处理过程
- 分析与字节码生产过程
其中关键的处理由8个方法来完成,来看一看。
2)解析与填充符号表
词法、语法分析
解析步骤由 parseFiles 方法完成。其中包括词法分析和语法分析两个过程。
词法分析是将字符流转为标记(Token)集合。
语法分析是根据token序列构造抽象语法树的过程。
填充符号表
完成了词法分析和语法分析后,就是填充符号表了。由 enterTrees方法完成。
符号表是由一组符号地址和符号信息构成的表格。
符号表中所登记的信息在编译的不同阶段都要用到。在语义分析阶段,用于语义检查和产生中间代码。在目标代码生成阶段,符号表是对符号名进行地址分配的依据。
3)注解处理器
注解与普通的java代码一样,是在运行期间发挥作用的。
在JDK 1.6 中,提供了一组插入式注解处理器的标准API 在编译期间对注解进行处理。它们可以读取、修改、添加抽象语法树中的任意元素。如果修改了语法树,编译器将回到解析及填充符号表过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止,每一次的循环称为一个 Round。
4) 语义分析与字节码生成
语法树能够表示一个结构正确的源程序的抽象,但是无法保证源程序是符号逻辑的。这时候就需要语义分析了。包含标注检查和数据及控制流分析两个过程。
标注检查
由attribute方法完成。
检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配。
数据及控制流分析
由flow方法完成。
这一步是对程序上下文逻辑更一步的验证,它可以检测出诸如程序局部吧在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理。
解语法糖
使用语法糖能够增加程序的可读性,从而减少代码出错的机会。
比如泛型、变长参数、自动装箱/拆箱等。
字节码生成
这个阶段,不仅仅是把起那么各个步骤所生成的信息转化为字节码写到磁盘上,编译器还进行了少了的代码添加和转换工作。
比如实例构造器 init 方法和类构造器 clinit 方法就是这个阶段添加到语法树之中的。
3. Java 语法糖的味道
语法糖,虽然不会提供实质性的功能改进,但是可以提高效率,或者提升语法的严谨性,或者减少编码出错的机会。
1)泛型与类型擦除
它的本质是参数化类型的应用,也就是说莎草纸的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。
Java 语言的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。
2)自动装箱、拆箱和遍历循环
装箱、拆箱在编译之后,被转化为对应的包装和还原方法,如Integer.valueOf Integer.intValue 方法。
循环遍历把代码还原成迭代器的实现。
3)条件编译
public static void main(String[] args) {
if (true) {
System.out.println("block 1");
} else {
System.out.println("block 2");
}
}
这段代码,在编译之后,就只有一段“System.out.println(“block 1”);”。
4. 实战:插入式注解处理器
1)实战目标
使用注解处理器API来编写一款拥有自己编码风格的校验工具:NameCheckProcessor。它主要做以下check:
- 类或接口:符合驼峰命名法,首字母大写
- 方法:符合驼峰命名法,首字母小写
- 字段:
- 类或者实例变量:符合符合驼峰命名法,首字母小写
- 常量:要求全部由大写字或下划线构成,并且第一个字符不能是下划线
2)代码实现
首先注解处理器的代码需要继承抽象类:javax.annotation.processing.AbstractProcessor,覆盖其中的abstract方法:process。
这个方法的第一个参数“annotations”中获取到此注解处理器所要处理的注解集合,从第二个参数“roundEnv”中访问到当前这个Round中的语法树节点,每个语法树节点在这里表示为一个Element。
// 可以用"*"表示支持所有Annotations
@SupportedAnnotationTypes("*")
// 只支持JDK 1.6的Java代码
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class NameCheckProcessor extends AbstractProcessor {
private NameChecker nameChecker;
/**
* 初始化名称检查插件
*/
@Override
public void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
nameChecker = new NameChecker(processingEnv);
}
/**
* 对输入的语法树的各个节点进行进行名称检查
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
for (Element element : roundEnv.getRootElements())
nameChecker.checkNames(element);
}
return false;
}
}
/**
* 程序名称规范的编译器插件:<br>
* 如果程序命名不合规范,将会输出一个编译器的WARNING信息
*/
public class NameChecker {
private final Messager messager;
NameCheckScanner nameCheckScanner = new NameCheckScanner();
NameChecker(ProcessingEnvironment processsingEnv) {
this.messager = processsingEnv.getMessager();
}
/**
* 对Java程序命名进行检查,根据《Java语言规范》第三版第6.8节的要求,Java程序命名应当符合下列格式:
*
* <ul>
* <li>类或接口:符合驼式命名法,首字母大写。
* <li>方法:符合驼式命名法,首字母小写。
* <li>字段:
* <ul>
* <li>类、实例变量: 符合驼式命名法,首字母小写。
* <li>常量: 要求全部大写。
* </ul>
* </ul>
*/
public void checkNames(Element element) {
nameCheckScanner.scan(element);
}
/**
* 名称检查器实现类,继承了JDK 1.6中新提供的ElementScanner6<br>
* 将会以Visitor模式访问抽象语法树中的元素
*/
private class NameCheckScanner extends ElementScanner6<Void, Void> {
/**
* 此方法用于检查Java类
*/
@Override
public Void visitType(TypeElement e, Void p) {
scan(e.getTypeParameters(), p);
checkCamelCase(e, true);
super.visitType(e, p);
return null;
}
/**
* 检查方法命名是否合法
*/
@Override
public Void visitExecutable(ExecutableElement e, Void p) {
if (e.getKind() == METHOD) {
Name name = e.getSimpleName();
if (name.contentEquals(e.getEnclosingElement().getSimpleName()))
messager.printMessage(WARNING, "一个普通方法 “" + name + "”不应当与类名重复,避免与构造函数产生混淆", e);
checkCamelCase(e, false);
}
super.visitExecutable(e, p);
return null;
}
/**
* 检查变量命名是否合法
*/
@Override
public Void visitVariable(VariableElement e, Void p) {
// 如果这个Variable是枚举或常量,则按大写命名检查,否则按照驼式命名法规则检查
if (e.getKind() == ENUM_CONSTANT || e.getConstantValue() != null || heuristicallyConstant(e))
checkAllCaps(e);
else
checkCamelCase(e, false);
return null;
}
/**
* 判断一个变量是否是常量
*/
private boolean heuristicallyConstant(VariableElement e) {
if (e.getEnclosingElement().getKind() == INTERFACE)
return true;
else if (e.getKind() == FIELD && e.getModifiers().containsAll(EnumSet.of(PUBLIC, STATIC, FINAL)))
return true;
else {
return false;
}
}
/**
* 检查传入的Element是否符合驼式命名法,如果不符合,则输出警告信息
*/
private void checkCamelCase(Element e, boolean initialCaps) {
String name = e.getSimpleName().toString();
boolean previousUpper = false;
boolean conventional = true;
int firstCodePoint = name.codePointAt(0);
if (Character.isUpperCase(firstCodePoint)) {
previousUpper = true;
if (!initialCaps) {
messager.printMessage(WARNING, "名称“" + name + "”应当以小写字母开头", e);
return;
}
} else if (Character.isLowerCase(firstCodePoint)) {
if (initialCaps) {
messager.printMessage(WARNING, "名称“" + name + "”应当以大写字母开头", e);
return;
}
} else
conventional = false;
if (conventional) {
int cp = firstCodePoint;
for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) {
cp = name.codePointAt(i);
if (Character.isUpperCase(cp)) {
if (previousUpper) {
conventional = false;
break;
}
previousUpper = true;
} else
previousUpper = false;
}
}
if (!conventional)
messager.printMessage(WARNING, "名称“" + name + "”应当符合驼式命名法(Camel Case Names)", e);
}
/**
* 大写命名检查,要求第一个字母必须是大写的英文字母,其余部分可以是下划线或大写字母
*/
private void checkAllCaps(Element e) {
String name = e.getSimpleName().toString();
boolean conventional = true;
int firstCodePoint = name.codePointAt(0);
if (!Character.isUpperCase(firstCodePoint))
conventional = false;
else {
boolean previousUnderscore = false;
int cp = firstCodePoint;
for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) {
cp = name.codePointAt(i);
if (cp == (int) '_') {
if (previousUnderscore) {
conventional = false;
break;
}
previousUnderscore = true;
} else {
previousUnderscore = false;
if (!Character.isUpperCase(cp) && !Character.isDigit(cp)) {
conventional = false;
break;
}
}
}
}
if (!conventional)
messager.printMessage(WARNING, "常量“" + name + "”应当全部以大写字母或下划线命名,并且以字母开头", e);
}
}
}
3)运行与测试
在执行javac命令时,通过“-processor”参数来执行编译时需要附带的注解处理器。
晚期(运行期)优化
1. 概述
当虚拟机发现某个方法或者代码块的运行特别频繁时,会通过即时编译器(Just In Time Complier, 简称JIT编译器),将这些“热点代码”(Hot Spot Code),编译成与本地平台相关的机器码,并进行各种层次的优化。
2. HotSpot 虚拟机内的JIT编译器
1)解释器与编译器
HotSpot 虚拟机采用解释器与编译器并存的架构。
当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。但随着运行时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码,来获取更高的执行效率。
当内存资源限制较大时,可以使用解释执行节约内存,反之可以使用编译执行来提升效率。
HotSpot 虚拟机内置了两个 JIT 编译器,分别称为 Client Compiler 和 Server Compiler,或者简称C1、C2编译器。
解释器与编译器搭配使用的方式在虚拟机中称为“混合模式”(Mixed Mode)。用户可以使用参数“-Xint”强制虚拟机运行于“解释模式”(Interpreted),也可以使用参数“-Xcomp”强制虚拟机运行于“编译模式”。
为了使程序启动相应速度和运行效率之间达到最佳平衡,HotSpot采用分层编译的策略:
- 第0层:程序解释执行,解释器不开启性能监控功能,可触发第1层编译
- 第1层:也称为C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要,加入性能监控的逻辑
- 第2层,也称C2编译,也是将字节码便以为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。
2)编译对象与触发条件
热点代码有两类:
- 被多次调用的方法
- 被多次执行的循环体
对于第一种情况,编译器会以整个方法作为编译对象,这种编译也是虚拟机中标准的JIT编译方式。后一种情况,尽管编译动作是由循环体所触发,但是仍然以整个方法作为编译对象,称为栈上替换(On Stack Replacement,简称OSR编译)。 判断一段代码是不是热点代码,是不是需要出发及时编译,这样的行为称为热点探测。目前主要分为两种:
- 基于采样的热点探测:虚拟机会周期性地检查各个线程的栈顶,如果发现某个方法经常出现在栈顶,那就是热点方法。优点是简单、高效,容易获取方法调用关系,确定是很难精确的确认一个方法的热度。
- 基于计数器的热点探测:为每个方法建立计数器,统计方法的执行次数,如果高于一定的阈值,就认为它是热点方法。缺点是实现麻烦一些,需要委会计数器,而且不能直接得到方法的调用关系,优点是统计结果更加精确和严谨。
HotSpot 采用第二种,因此它为每个方法准备了两类计数器:方法调用计数器和回边计数器。
方法调用计数器默认阈值,在client模式下是1500次,在Server模式下是10000次。这个值可以通过“-XX:CompileThreshold”来人为设定。
当超过一定时间,如果方法调用的次数不能超过阈值,计数器的值就会衰减一般,这个称谓方法调用计数器热度的衰减。
回边计数器的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”。
3)编译过程
对于Client Compiler来说,是一个简单快速的三段式编译器,主要关注点在局部性的优化,而放弃了许多耗时较长的全局优化手段。具体过程:字节码 -> 高级中间代码HIR -> 低级中间代码LIR -> 机器代码。
Server Compiler是一个充分优化过的高级编译器,它会执行所有经典的优化动作,如无用代码消除、循环展开、循环表达式外提、消除公共子表达式、常量传播、基本块重排序等,还有与java相关的范围检查消除、空值检查消除等。
3. 编译优化技术
语言无关的经典优化技术之一:公共子表达式消除
Before:
int d = e * 12 + a + (a + e);
After:
int d = e * 13 + a *2;
语言相关的经典优化技术之一:数组范围检查消除
Before:
if(foo != null){
return foo.value;
} else{
throw new NullPointException();
}
After:
try{
return foo.value;
} catch(Exception e){
throw e;
}
最重要的优化技术之一:方法内联
把目标方法的代码“复制”到发起调用的方法之中,避免发生真的方法调用。
最前沿的优化技术之一:逃逸分析
当一个对象在方法中定义之后,它可能被外部方法所引用,这就成为方法逃逸。如果被其他线程访问,成为线程逃逸。