编译期优化
Java语言的编译期是一段不确定的操作过程,因为它可能是指一个前端编译器(或叫编译器的前端)把 .java文件 转变成 .class文件 的过程;也可能是虚拟机的后端运行期编译器(JIT编译器,Just in Time Compiler)把字节码抓变成机器码的过程;还可能是指使用静态提前编译器(AOT编译器,Ahead Of Time Compiler) 直接把 *.java 文件编译成本地机器代码的过程。具体有:
- 前端编译器:Sun的Javac、Eclipse JDT中的增量式编译器(ECJ);
- JIT编译器:HotSpot VM的C1、C2编译器;
- AOT编译器:GNU Compiler for the Java(GCJ)、Excelsior JET。
1. Sun Javac 编译过程
虚拟机严格定义了Class文件的格式,但并没有对如何把Java源码转变为Class文件的编译过程进行严格的定义。某些情况下,会出现Javac编译器可以编译而ECJ编译器不可编译的问题。
对Sun Javac而言,编译过程如图所示:
主体代码如下:
1.1 解析和填充符号表
词法分析
词法分析是将源代码的字符流转变为标记(Token)集合,标记是编译过程的最小元素,如:关键字、变量名、字面量、运算符。
语法分析
语法分析是根据Token序列构造抽象语法树(Abstract Syntax Tree,AST)的过程,AST用来描述程序代码语法结构的树形表示方式,每一节点代表程序代码中的一个语法结构
填充符号表
符号表(Symbol Table)是由一组符号地址和符号信息构成的表格,类似哈希表的K-V形式。在语义分析阶段,符号表用来语义检查和产生中间代码;在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的依据。
1.2 注解处理器
在JDK5之后,Java提供了注解(Annotation)的支持,这些注解和普通的Java代码一样,在运行期发挥作用。在JDK6中,提供的插入时注解处理器在编译期对注解进行处理,可以把插入式注解处理器看成一组编译器的插件,能够插入、修改、读取抽象语法树的任一元素。每次处理称为一个Round,也就是Sun Javac编译过程中的回环。
1.3 语义分析
语义分析的主要任务是对结构上正确的源程序进行上下文有关性质的检查,包括标注检查和数据及控制流检查。
标注检查:标注检查的步骤包括如变量使用前是否声明、变量与赋值类型是否匹配等,以及需要特别说明的常量折叠。如果在代码中写了int _2kb = 1024 * 2;
,经常量折叠后,表达式的值会在语法树上标注出来(ConstantValue:2048),由于在编译期进行了常量折叠,并不会在运行期增加CPU的运算量。
数据及控制流分析:数据及控制流分析是对程序上下文逻辑更进一步的验证,可以检查出如程序局部变量在使用前是否赋值、方法的每条路径是否都有返回值、是否所有受查异常都被正确处理等。编译期的数据及控制流分析与类加载时的数据及控制流分析基本一致,但校检范围有所区别,有一些校检项只在编译期或运行期进行。
解语法糖:语法糖(Syntactic Sugar),也称糖衣语法。指计算机语言中添加的某种语法,这种语法对语言功能并没有影响,但更方便程序员的使用,增加程序可读性,在Java中有如:泛型、变长参数、自动装箱/拆箱等。在编译阶段将语法糖还原回简单的基础语法结构,称为解语法糖。
1.4 字节码生成
Javac编译的最后一个阶段,将生成的语法树、符号表转化成Class文件,以及少量的代码添加、转换工作,如实例构造器<init()>
和类构造器<clinit()>
就是在这阶段添加到语法树中。
2. Java语法糖——糖衣背后的真实
语法糖虽然不会提供实质性的功能改进,但它们或能提高效率,或能提升语法的严谨性,或能减少编码出错的机会。但我们也要认清糖衣背后的真实,看清程序的真面目。
2.1 泛型与类型擦除
泛型是JDK5的一项新特性,本质是参数化类型(Parametersized Type)的应用。在Java语言还没有泛型的时候,通过Object是所有类型的父类以及强制类型转换配合使用实现泛型化。在C#和Java中的泛型有着根本性的区别,C#的泛型无论在源码中、编译后、运行时都是真实存在的,List<Integer>
和List<String>
是两个不同的类型,在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,;Java的泛型只存在于源码中,编译后的Class文件中已经变为原生类型了,并且在相应地方加入了强制类型转换,对于运行期的Java语言来说,List<Integer>
和List<String>
是同一个类,这种实现方法称为类型擦除,属于伪泛型。
下面举一个例子说明,类型擦除前的代码:
由编译出的Class文件,再反编译回去的代码:
当泛型遇上重载,如下代码:1
2
3
4
5
6
7public void method(List<String> list) {
...
}
public void method(List<Integer> list) {
...
}
是不能通过编译的,因为类型擦除后,方法的参数都是List(对于JDK6而言,如果返回类型不一样是能够编译的,原因在于描述符不同的方法在Class文件中能够共存,之后的版本不可编译)。
另外类型擦除通过Java虚拟机规范后来加入的两个属性:Signature和LocalVariableTypeTable,可以通过反射获得实际的类型参数而不是原生类型。
2.2 遍历循环与变长参数
遍历循环(Foreach)使用关键字for( : )
实现,变长参数指方法的参数长度不定,如下代码:1
2
3
4List<Integer> list = Arrays.asList(new Integer(1), new Integer(2), new Integer(3));
for (Integer i : list) {
System.out.println(i.toString());
}
由编译出的Class文件,再反编译回去的代码:1
2
3
4
5
6
7
8
9
10List list = Arrays.asList(new Integer[] {
new Integer(1),
new Integer(2),
new Integer(3)
});
for (Iterator localIterator = list.iterator(); localIterator.hasNext(); ) {
Integer i = ((Integer)localIterator.next());
System.out.println(i.toString());
}
显然,遍历循环是依赖Iterable接口实现的,而变长参数语法是通过数组完成的。
2.3 自动装箱、自动拆箱与“==”、“equals()”
自动装箱指的是用基本类型的值给它的包装类赋值时,系统会自动将基本类型转换成它的包装类型。实际上,系统自动调用Integer.valueOf()方法来完成转换。JDK源码如下:1
2
3
4
5public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
先判断能不能冲缓存中获取,否则new一个Integer对象。
关于IntegerCache
的源码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
意思是,默认情况下创建一个Integer数组的缓存,默认情况下该缓存的内容是值为-128到127的Integer对象。
自动拆箱和自动装箱相反,当需要基本类型的时候,系统也会自动的将包装类转换成基本类型。实际上,系统自动调用Integer.intValue方法来完成转换。JDK源码如下:1
2
3public int intValue() {
return value;
}
再来看下“==”和“equals()”
==
运算符,如果是基本数据类型,则直接对值进行比较,如果是引用数据类型,则是对他们的地址进行比较(但是只能比较相同类型的对象,或者比较父类对象和子类对象。类型不同的两个对象不能使用==)equals()
方法,继承自Object类,在具体实现时可以覆盖父类中的实现。看一下Object中equals的源码发现,它的实现也是对对象的地址进行比较,此时它和”==”的作用相同。而JDK类中有一些类覆盖了Object类的equals()方法,比较规则为:如果两个对象的类型一致,并且内容一致,则返回true,这些类有:
java.io.file,java.util.Date,java.lang.string,包装类(Integer,Double等)。
看如下代码,思考输出:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public static void main(String args[]) {
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
int x = 3;
long y = 3L;
//x,y虽然类型不同但是可以直接进行数值比较
System.out.println(x == y); //true,int类型向long类型对齐,从二进制角度表示来看,显然值相等
//System.out.println(c == g); 提示出错,不可比较的类型。说明此时没有自动拆箱
System.out.println(c == d); //true,两个在IntegerCache范围的的Integer类型进行比较,同一个引用地址进行比较
System.out.println(e == f); //false,两个不在IntegerCache范围的的Integer类型进行比较,不同引用地址进行比较
System.out.println(c == (a+b)); //true,有“+”运算符,“a+b”自动拆箱后进行运算,变量“c”也自动拆箱,对值进行比较
System.out.println(c.equals(a+b)); //true,有“+”运算符,“a+b”自动拆箱后进行运算,对值进行自动装箱,对对象类型以及值进行比较
//此时进行了自动的拆箱
System.out.println(g == (a+b)); //true,有“+”运算符,“a+b”自动拆箱后进行运算,对值进行比较
System.out.println(g.equals(a+b)); //false,有“+”运算符,“a+b”自动拆箱后进行运算,对值进行自动装箱,对对象类型以及值进行比较
}
2.4 条件编译
Java语言也有条件编译,方法是也只是条件为常量的if语句,具体例子如下代码:1
2
3
4
5
6
7public static void main(String[] args) {
if (true) {
System.out.println("block1");
} else {
System.out.println("block2");
}
}
由编译出的Class文件,再反编译回去的代码如下:1
2
3public static void main(String[] args) {
System.out.println("block1");
}
由于使用了if语句,所以只能写在方法体内,实现语句块(Block)级别的条件编译,而没办法实现根据条件调整整个Java类的结构。
Copyright © 2018, GDUT CSCW back-end Kanarien, All Rights Reserved