----------------------------编译期优化--------------------------------------
1、概述(编译期)
Java语言的“编译期”其实是一段“不确定”的操作过程,因为它可能是指一个前端编译器(Javac编译器)把Java文件转变为class文件的过程;也可能是指虚拟机的后端运行期编译器(JIT编译器)把字节码转变为机器码的过程;还可能是指使用静态提前编译器(AOT编译器,Ahead of Time Compiler)直接把Java文件编译成本地机器码的过程。下面列举这三类编译过程中一些比较有代表性的编译器。
a)前端编译器:Sun的Javac、Eclipse JDT中的增量式编译器(ECJ);【编译期、Javac编译器用Java语言实现】
b)JIT编译器:HotSpot VM的C1、C2编译器;【运行期】
c)AOT编译器:GNU Compiler for the Java 、Excelsior JET;
优化:Javac这类编译器对代码的运行效率几乎没有任何优化措施。虚拟机设计团队把对性能的优化集中到了后端的即时编译器中,这样可以让那些不是由Javac产生的Class文件(例如Groovy、JRuby等语言的Class文件)也同样享受到编译器优化带来的好处。但是Javac做了许多针对Java语言编码过程的优化措施来改善程序猿的编码风格和提高编码效率,相当多新生的Java语法特性都是靠编译器的“语法糖”来实现的,而不是依赖虚拟机的底层改进来支持。
可以说,Java中即时编译器在运行期的优化过程对于程序运行来说更重要,而前端编译器在编译期的优化过程对于程序编码来说关系更密切。
2、语法糖的味道
语法糖:也成为糖衣语法,是指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序猿的使用。通常来说,使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。
Java中最常用的语法糖主要是泛型(泛型并不一定都是语法糖实现,如c#的泛型就是直接由CLR支持的)、变长参数、自动装箱/拆箱等,虚拟机运行时不支持这些语法,它们在编译阶段还原回简单的基础语法结构,这个过程称为解语法糖。
2.1、泛型与类型擦出
泛型技术在c#和Java之中的使用方式看似相同,但实现上却有着根本性的分歧,C#里面的泛型无论在程序源码中、编译后的IL中,或是运行期的CLR中,都是切实存在的,List<int>和List<String>就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型。
Java语言中的泛型则是不一样的,它只在程序的源码中存在,在编译后的字节码文件中就已经被替换为原来的原生类型了,并且在相应的地方插入了强制转型代码。因此,对于Java语言来说,ArrayList<int>和ArrayList<String>就是同一个类,所以泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦出,基于这种方法实现的泛型被称为伪泛型。
---------------泛型擦出前-----------------
Map<String, String> map = new HashMap<>();
map.put("hello", "你好");
map.put("how are you", "吃了没?");
System.out.println(map.get("hello"));
System.out.println(map.get("how are you"));
---------------泛型擦出后-----------------
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"));
a)当泛型遇见重载1
public class GenericTypes{
public static void method(List<String> list){
System.out.println("invoke method(List<String> list)");
}
public static void method(List<Integer> list){
System.out.println("invoke method(List<Integer> list)");
}
}
上述这段代码是不能被编译的,因为参数List<String>和List<Integer>编译之后都被擦除了,变成了一样的原生类型List<E>,擦出动作导致这两个方法的特征签名变得一模一样。但是只能说,泛型擦除成相同的原生类型只是无法重载的其中一部分原因。
b)当泛型遇见重载2
public class GenericTypes {
public static String method(List<String> list) {
System.out.println("invoke method(List<String> list)");
return "";
}
public static int method(List<Integer> list) {
System.out.println("invoke method(List<Integer> list)");
return 0;
}
public static void main(String[] args) {
method(new ArrayList<String>());
method(new ArrayList<Integer>());
}
}
上述方法的重载由于返回值的加入,居然成功了。why?当然不是根据返回值来确定的,之所以能编译和执行成功,是因为两个method方法加入不同的返回值才能共存在一个Class文件之中。方法重载要求方法具备不同的特征签名,返回值并不包含在方法的特征签名之中,所以返回值不参与重载选择,,但是在Class文件格式之中,只要描述符不是完全一致的两个方法就可以共存。也就是说,两个方法如果有相同的名称和特征签名,但是返回值不同,那它们也是可以合法的共存于一个Class文件中的。
由于Java泛型的引入,各种场景(虚拟机解析、反射等)下的方法调用都可能对原有的基础产生影响和新的需求,如泛型类中如何获取传入的参数化类型等。因此,JCP组织对于虚拟机规范也做出了相应的修改,引入诸如Signature、LocalVariableTypeTable等新的属性用于解决伴随着泛型而来的参数类型的识别问题,Signature是其中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名,这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息。修改后的虚拟机规范要求所有能识别49.0以上版本的Class文件的虚拟机都要能正确的识别Signature参数。
从上面的例子中可以看到擦出法对实际编码带来的影响,由于List<String>和List<Integer>擦出后是同一个类型,我们只能添加两个不同的返回值才能完成重载,这是一种毫无优雅和美感可言的解决方案。
另外,从Signature属性的出现我们还可以得出结论,擦除法仅仅是方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。
2.2、自动装箱、拆箱与遍历循环
自动装箱、拆箱与遍历循环无论是实现还是思想上都不能和泛型相比。这里专门作为一小节,是因为它们是Java中使用最多的语法糖。
-------自动装箱、拆箱与遍历循环--------
public static void main(String[] args) {
//asList(T... a)
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<Integer> 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);
}
上述示例包含了泛型、自动装箱、自动拆箱、遍历循环与变长参数5种语法糖。泛型不必说了,自动装箱/拆箱在编译后被转化成了对应的包装和还原方法,而遍历循环则把代码还原成了迭代器的实现,这也是为何遍历循环需要被遍历的类实现Iterable接口的原因。最后再看看变长参数,它在调用的时候变成了一个数组类型的参数。
----------------------------运行期优化--------------------------------------
3、概述(运行期)
- 为何HotSpot虚拟机要使用解释器与编译器并存的架构?
- 为何HotSpot虚拟机要实现两个不同的即时编译器?
- 程序何时使用解释器执行?何时使用编译器执行?
- 哪些程序代码会被编译为本地代码?为何要编译为本地代码?
- 如何从外部观察即时编译器的编译过程和编译结果?
3.1、解释器与(JIT)编译器
许多主流的商用虚拟机,如HotSpot、J9等,都同时包含解释器与编译器。解释器与编译器两者各有优势:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,获取更高的执行效率。3.2、优化技术
这个地方只提供几个关键词,具体看参阅书中章节。
最具代表性的几项技术:
1、公共子表达式消除;2、数组范围检查消除;3、方法内联;4、逃逸分析;