Java高级——前端编译与优化
前端编译与优化
概述
前端编译器把java文件编译成class文件,如:JDK的Javac、Eclipse JDT中的增量式编译器(ECJ)
- 前端编译器对代码的运行效率无任何优化措施
- 性能优化集中到运行期的即时编译器中,这样可适用于非Javac生成的Class文件
- 但有针对编码过程的优化,从而降低编码复杂度,如语法糖
《Java虚拟机规范》未规定如何将java文件编译为Class文件,给予了较大的灵活性
编译过程与具体编译器实现绑定,可能会导致某些代码在Javac中可以编译,而在ECJ则不可编译
Javac导入
Javac编译器由纯Java实现
- 源码放在/langtools/src/share/classes/com/sun/tools/javac
- JDK9后放在/src/jdk.compiler/share/classes/com/sun/tools/javac
下面导入JDK8中的Javac源码,新建项目,将源码langtools/src/share/classes中的com/sun复制到项目目录(根据提示将private改为package即可解决报错)
将Run-RunConfiguration-Main中的Mian Class指定为javac.Main
新建一个Test.java,在Argument-Program argument指定其路径
点击Run,即可编译出Test.class,与在命令行中运行javac Test.java效果是一样的
Javac编译过程
- 初始化插入式注解处理器
- 解析与填充符号表
- 注解处理
- 分析与字节码生成
Javac编译动作的入口是
com.sun.tools.javac.main.JavaCompiler类,关键代码如下
解析与填充符号表
解析
解析由parseFiles()方法完成,包括词法分析和语法
词法分析将源代码的字符流转变为标记(Token)集合
- 字符是程序最小元素,但标记才是编译时的最小元素
- 关键字、变量名、字面量、运算符都可以作为标记
- 由com.sun.tools.javac.parser.Scanner类实现
- 如int a=b+2包含了6个标记,分别是int、a、=、b、+、2
语法分析根据标记序列构造抽象语法树
- 抽象语法树(Abstract Syntax Tree,AST)是一种用来描述程序代码语法结构的树形表示方式
- 其节点都代表着程序代码中的一个语法结构(Syntax Construct),如包、类型等
- 由com.sun.tools.javac.parser.Parser类构造,com.sun.tools.javac.tree.JCTree类表示
如下是Eclipse AST View插件分析出来的某段代码的抽象语法树视图
生成语法树以后,编译器不会再对源码字符流进行操作了,后续操作都建立在抽象语法树之上
填充符号表
填充符号表由enterTrees()方法完成,符号表(Symbol Table)是由一组符号地址和符号信息构成的数据结构
- 在语义分析阶段用于语义检查(如检查一个名字的使用和原先的声明是否一致)和产生中间代码
- 在目标代码生成阶段根据符号表对符号名进行地址分配
- 由com.sun.tools.javac.comp.Enter类实现
- 生成一个待处理列表,包含了每一个编译单元的抽象语法树和package-info.java的顶级节点
注解处理过程
注解处理由processAnnotations()方法完成,JDK6之前注解只会在程序运行时发挥作用,JDK6设计了插入式注解处理器API将注解处理提前至编译期
可把插入式注解处理器看作编译器的插件,允许读取、修改、添加抽象语法树中的任意元素,如注解来实现自动产生getter/setter方法、生成受查异常表等
若在处理注解期间修改过语法树,编译器将回到解析及填充符号表的过程重新处理,直至不再修改语法树,循环过程称为一个轮次(Round)
若有新的注解处理器,会通过com.sun.tools.javac.processing.JavacProcessing-Environment类的doProcessing()方法生成一个新的JavaCompiler对象
分析和字节码生成
分析和字节码生成由Compile2()方法完成,包括标注检查、数据流及控制流分析、解语法糖、字节码生成
分析指语义分析,对结构上正确的源程序进行上下文相关性质的检查
抽象语法树能够表示一个结构正确的源程序,但无法保证源程序的语义是符合逻辑的,如下
int a = 1;
boolean b = false;
char c = 2;
可能出现
int d = a + c;
int d = b + c;
char d = a + c;
上面3种赋值都能构成正确的抽象语法树,但只有第一种赋值在Java语义上是正确的(IDE的红线错误提示即来源于语义分析)
标注检查
标注检查由attribute()完成,实现类是com.sun.tools.javac.comp.Attr和com.sun.tools.javac.comp.Check
标注检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配
在标注检查中,会进行一个称为常量折叠(Constant Folding)的代码优化,如对于
int a = 1 + 2;
在抽象语法树上仍然能看到字面量“1”“2”和操作符“+”号,但是在经过常量折叠优化之后,会变成字面量“3”,
数据流及控制流分析
数据流及控制流分析由flow()方法完成,实现类为com.sun.tools.javac.comp.Flow
对程序上下文逻辑更进一步的验证,如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值等
编译时和类加载时的数据及控制流分析基本一样,但校验范围有所区别,有一些校验项只能在编译期或运行期进行
public void foo(final int arg) {
final int var = 0;
}
public void foo(int arg) {
int var = 0;
}
如上,一个方法有final,一个没有,但它们编译出来的指令都是一模一样
- 局部变量在常量池中并没有CONSTANT_Fieldref_info
- 也无存储有访问标志(access_flags)的信息
- 故Class文件不会知道局部变量是否为final
- final的不可变性仅由Javac在编译期间保证
解语法糖
解语法糖由desugar()方法完成,实现类为com.sun.tools.javac.comp.TransTypes和com.sun.tools.javac.comp.Lower
语法糖指在计算机语言中添加的某种语法,其对语言的编译结果和功能并没有实际影响,但是却能更方便使用该语言
Java中的语法糖如泛型、变长参数、自动装箱拆箱等,JVM运行时并不直接支持这些语法,需在编译阶段将其还原回原始的基础语法结构,称为解语法糖
字节码生成
字节码生成由generate()方法完成,实现类为com.sun.tools.javac.jvm.Gen
把前面各个步骤所生成的信息(语法树、符号表)转化成字节码指令写到磁盘中,还进行了代码添加(<init>)和转换(将+转为StringBuffer的append)
完成对语法树的遍历和调整之后,就会把的符号表交到com.sun.tools.javac.jvm.ClassWriter的writeClass()方法输出字节码,生成Class文件
前端编译优化
泛型
泛型将操作的数据类型指定为方法签名中的一种特殊参数,用于类、接口和方法的创建中,分别构成泛型类、泛型接口和泛型方法
Java和C#的泛型
C#是具现化式泛型(Reified Generics)
- 在源码、编译后的中间语言或是运行期都是实际存在的
- 运行期的List<int>与List<string>是两个不同的类型,有独立的虚方法表和类型数据
Java是类型擦除式泛型(Type Erasure Generics)
- 只在程序源码中存在,在Class中泛型被替换为原来的裸类型(Raw Type),并且加入强制转型代码
- 运行期的ArrayList<int>与ArrayList<String>是同一个类型
Java泛型使用效果(无法instanceof、创建对象和数组)和运行效率(需不断拆箱装箱)都不如C#,唯一优势在于实现只需要修改Javac,不需要改动字节码和JVM,也保证了以前没有使用泛型的库也可运行在JDK5之上
泛型擦除
为了保证兼容性,定义裸类型为所有该类型泛型化实例的共同父类型(Super Type),如下
ArrayList<Integer> ilist = new ArrayList<Integer>();
ArrayList<String> slist = new ArrayList<String>();
ArrayList list; // 裸类型
list = ilist;
list = slist;
在编译时把ArrayList<Integer>还原回ArrayList,在元素访问、修改时自动插入一些强制类型转换和检查指令
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("hello", "你好");
map.put("how are you?", "吃了没?");
System.out.println(map.get("hello"));
System.out.println(map.get("how are you?"));
}
对于如上代码,编译成Class,再反编译,程序变回泛型出现之前的写法,泛型都变回了裸类型,在元素访问时插入强制转型代码
public static void main(String[] args) {
Map map = new HashMap();
map.put("hello", "你好");
map.put("how are you?", "吃了没?");
System.out.println((String) map.get("hello"));
System.out.println((String) map.get("how are you?"));
}
泛型缺点
泛型擦除无法支持原始类型,如下
ArrayList<int> list = new ArrayList<int>();
导致只能使用包装类型,运行速度慢
ArrayList<Integer> list = new ArrayList<Integer>();
运行时无法获取泛型类型信息
if (item instanceof E) { // 不合法,无法对泛型进行实例判断
}
E newItem = new E(); // 不合法,无法使用泛型创建对象
E[] itemArray = new E[10]; // 不合法,无法使用泛型创建数组
如写一个泛型版本的从List到数组的转换方法,需显式传递数组的组件类型componentType
public static <T> T[] convert(List<T> list, Class<T> componentType) {
T[] array = (T[])Array.newInstance(componentType, list.size());
}
为了在反射中获取参数化类型,新增Signature属性用于存储一个方法在字节码层面的特征签名,其包含了参数化类型的信息
其他
如下,包含泛型、自动装箱、自动拆箱、遍历循环与变长参数5种语法糖
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4);
int sum = 0;
for (int i : list) {
sum += i;
}
System.out.println(sum);
}
反编译后
- 自动装箱、拆箱转化为对应的包装和还原方法
- 遍历循环把代码还原成了迭代器实现
- 变长参数变成了一个数组类型的参数
public static void main(String[] args) {
List list = Arrays.asList( new Integer[] {
Integer.valueOf(1),
Integer.valueOf(2),
Integer.valueOf(3),
Integer.valueOf(4) });
int sum = 0;
for (Iterator localIterator = list.iterator(); localIterator.hasNext(); ) {
int i = ((Integer)localIterator.next()).intValue();
sum += i;
}
System.out.println(sum);
}
条件编译
使用条件为常量的if语句,会触发条件编译,如下
public static void main(String[] args) {
if (true) {
System.out.println("block 1");
} else {
System.out.println("block 2");
}
}
在编译时,就会优化
public static void main(String[] args) {
System.out.println("block 1");
}
若非if语句,则可能在控制流分析中提示错误且拒绝编译,如下
public static void main(String[] args) {
// 编译器将会提示“Unreachable code”
while (false) {
System.out.println("");
}
}
实战:插入式注解处理器
在前端编译这部分,可通过插入式注解处理器API影响Java编译子系统的行为
接下来基于插入式注解处理器API编写一个校验工具NameCheckProcessor规范书写格式:
- 类(或接口):驼式命名法,首字母大写
- 方法:驼式命名法,首字母小写
- 类或实例变量:符合驼式命名法,首字母小写。
- 常量:由大写字母或下划线构成,首字符不能是下划线
代码实现
继承抽象类javax.annotation.processing.AbstractProcessor,实现抽象方法process(),其是Javac在执行注解处理器代码时的回调,如下,把当前轮次中的每一个RootElement传递到NameChecker执行名称检查逻辑
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
@SupportedAnnotationTypes("*") //设置要处理的注解
@SupportedSourceVersion(SourceVersion.RELEASE_6) //设置处理的Java版本
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; //不需要改变或添加抽象语法树,返回false,编译器不构造新的JavaCompiler实例
}
}
- 域processingEnv是父类中的一个protected变量,用于创建新的代码、向编译器输出信息、获取其他工具类等
- visitType()、visitVariable()和visitExecutable()访问类、字段和方法
- checkCamelCase()与checkAllCaps()检查驼式命名法和全大写命名
import java.util.EnumSet;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.ElementScanner6;
import javax.tools.Diagnostic;
public class NameChecker {
private final Messager messager;
NameCheckScanner nameCheckScanner = new NameCheckScanner();
NameChecker(ProcessingEnvironment processsingEnv) {
this.messager = processsingEnv.getMessager();
}
public void checkNames(Element element) {
nameCheckScanner.scan(element);
}
/**
* 名称检查器实现类,继承了JDK 6中的ElementScanner6
* 将会以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() == ElementKind.METHOD) {
Name name = e.getSimpleName();
if (name.contentEquals(e.getEnclosingElement().getSimpleName()))
messager.printMessage(Diagnostic.Kind.WARNING, "一个普通方法 “" + name + "”不应当与类名重复,避免与构造函数产生混淆", e);
checkCamelCase(e, false);
}
super.visitExecutable(e, p);
return null;
}
/**
* 检查变量命名是否合法
*/
@Override
public Void visitVariable(VariableElement e, Void p) {
// 如果这个Variable是枚举或常量,则按大写命名检查,否则按照驼式命名法规则检查
if (e.getKind() == ElementKind.ENUM_CONSTANT || e.getConstantValue() != null || heuristicallyConstant(e))
checkAllCaps(e);
else
checkCamelCase(e, false);
return null;
}
/**
* 判断一个变量是否是常量
*/
private boolean heuristicallyConstant(VariableElement e) {
if (e.getEnclosingElement().getKind() == ElementKind.INTERFACE)
return true;
else if (e.getKind() == ElementKind.FIELD && e.getModifiers().containsAll(EnumSet.of(Modifier.PUBLIC, Modifier.STATIC, Modifier.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(Diagnostic.Kind.WARNING, "名称“" + name + "”应当以小写字母开头", e);
return;
}
} else if (Character.isLowerCase(firstCodePoint)) {
if (initialCaps) {
messager.printMessage(Diagnostic.Kind.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(Diagnostic.Kind.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(Diagnostic.Kind.WARNING, "常量“" + name + "”应当全部以大写字母或下划线命名,并且以字母开头", e);
}
}
}
验证
使用如下代码验证
public class BADLY_NAMED_CODE {
enum colors {
red, blue, green;
}
static final int _FORTY_TWO = 42;
public static int NOT_A_CONSTANT = _FORTY_TWO;
protected void BADLY_NAMED_CODE() {
return;
}
public void NOTcamelCASEmethodNAME() {
return;
}
}
利用上面导入的Javac,参数-processor指定执行编译时需要附带的注解处理器(有多个用逗号分隔)
javac src/com/sun/NameChecker.java
javac src/com/sun/NameCheckProcessor.java
javac -processor com.sun.NameCheckProcessor src/com/sun/BADLY_NAMED_CODE.java
输出如下