编译期优化

对效率的追求是程序员天生的坚定信仰


编译期的前端(如Sun的Javac)把*.java文件转化成*.class文件,后端运行期编译器(JIT编译器,如HotSpot VM的C1、C2编译器)把字节码转化成机器码,静态提前编译器(GCJ)直接把*.java文件编译为本地机器代码,本文所讲的都是前端编译部分

Javac并没有优化措施,对性能的优化多集中在后端的即时编译器中,但Javac做了优化措施来改善程序员的编码风格和提高编码效率,许多新生Java“语法糖”,如泛型、内部类,是靠前端编译器实现的,而不是依赖虚拟机的底层改进,且本身就是由Java编写的,为分析源码提供便利。语言编译器竟然是由语言本身完成,称为自举,这似乎让自己有点明白了中最后两章自制编译器,即最开始使用X语言(如Fortran)实现Y语言(如Pascal)的编译器,即解决鸡与蛋的问题,即使用其它语言构建出我们的第一版编译器,之后成熟以后,就可以完全使用已经生成好的编译器来编译出我们的新编译器

编译过程大概分成三步

  • 解析与填充符号表:解析分为词法分析语法分析,词法分析是将源代码的字符流转变为Token集合,Token指的是关键词、变量名、字面量、运算符等,词法分析由javac.parser.Scanner类完成;语法分析则根据Token序列构造抽象语法树,具体可参见,语法分析由javac.parser.Parser类完成,生成的抽象语法树由javac.tree.JCTree类表示
  • 插入式注解处理器的注解处理:编写注解处理器参见第20章 注解,如果这些注解处理器在处理注解期间对语法树进行了修改,编译器将回到上一步骤重新处理,直到不再对语法树进行修改为止,每次循环成为一个Round
  • 分析和字节码生成:语法分析形成的抽象树能表示正确的源程序抽象,但不代表符合语言规定的语义,因此要进行语义分析,而语义分析必须限定在具体的语言和具体的上下文环境中

Javac的语义分析过程分为标注检查控制流分析两个步骤

  • 标注检查:变量是否已被申明过,变量与赋值之间的数据类型是否匹配等,除此之外,还有一个常量折叠步骤,比如int a = 1 + 3,在这个步骤之后,语法树上就会呈现出int a = 3,因此在运行期间并不会增加CPU指令,由javac.comp.Attr和javac.comp.Check类实现
  • 数据及控制流分析:程序局部变量再使用前是否有赋值、方法的每条路径是否都是有返回值、是否所有的受查异常都正确处理等,局部变量的final不变性是由编译器在编译期间保障,而不是在运行期,因为有无final得到的Class文件都是一样,由javac.comp.Flow实现???

语法糖对语言的功能并没有影响,但更方便程序员使用,增加程序的可读性,减少出错的可能性。Java是低糖语言,主要语法糖有泛型、变长参数、自动装箱/拆箱等,这些语法糖在编译期间会被还原为简单的基础语法结构,因为JVM并不支持这些语法,由javac.comp.TransTypes类和javac.comp.Lower类完成

最后一个阶段是字节码生成,这个阶段把前面步骤产生的信息转化为字节码写入到磁盘中,除此之外,编译器还进行了少量的代码添加和转换工作,由javac.jvm.Gen类实现,实例构造器<init>()方法和类构造器<clinit>()方法就是在这个阶段添加到语法树之中的(这里的实例构造器并不是指默认的构造函数,而是指我们自己重载的构造函数,如果用户代码中没有提供任何构造函数,那编译器会自动添加一个没有参数、访问权限与当前类一致的默认构造函数,这个工作在填充符号表阶段就已经完成了)???把字符串的加操作替换为StringBuffer或StringBuilder的append()就是在这个步骤完成

Java的泛型只在程序源码中存在,在编译后的字节码文件中,就已经替换成原来的原生类型(如整型、字节型,区别于引用类型),实现Java泛型的方法称为类型擦除,这种泛型称为伪泛型,下面是自动装箱的一个陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class test {
public static void main(String[] agrs){
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 4;
Integer e = 321;
Integer f = 321;
Long g = 3L;
System.out.println(c == d);//指向同一个对象
System.out.println(e == f);//值大于127,不同地址
System.out.println(c == (a + b));//自动拆箱,比较数值
System.out.println(c.equals(a + b));//类型和值都相同
System.out.println(g == (a + b));//自动拆箱,比较数值
System.out.println(g.equals(a + b));//值相同,类型不同
}
}

true
false
true
true
true
false

“==”运算:如果是基本数据类型,则直接对值进行比较,如果是引用数据类型,则是对他们的地址进行比较(但是只能比较相同类型的对象,或者比较父类对象和子类对象。类型不同的两个对象不能使用==)。包装类在不遇到算术运算的情况下不会自动拆箱

Object中qeuals的源码发现,它的实现也是对对象的地址进行比较,此时它和”==”的作用相同。而JDK类中有一些类覆盖了Object类的equals()方法,比较规则为:如果两个对象的类型一致,并且内容一致,则返回true,这些类有:java.io.file、java.util.Date、java.lang.string、包装类(Integer,Double等)

nteger使用一个内部静态类中的一个静态数组保存了-128-127范围内的数据,静态数组在类加载以后是存在方法区,并不是什么常量池。在自动装箱的时候,首先判断要装箱的数字的范围,如果在-128-127的范围则直接返回缓存中已有的对象,否则new一个新的对象。其他的包装类也有类似的实现方式