Java程序编译和代码优化

本文对应《深入理解java虚拟机》第四部分,即第十章和第十一章,主要讲了编译器相关的部分。

前端编译

本书一共介绍了三种编译器,分别是前端编译器(负责把.java文件编译成.class文件),即时编译器(把字节码文件编译成本地机器码)和提前编译器(直接把java代码编译成机器码)。接下里来的内容就是对javac这款用java写的编译器进行的分析。

编译过程

由于真的是没学过《编译原理》,那些什么词法分析,语法分析是真的看不懂,所以这里就直接把这些过程直接省略了,这部分我的重点是对一些语法糖的理解。

注解处理器

目前用到的几乎所有框架,都可以通过@xxx来进行注解式编程,极大地提升了我们的效率。而这些注解,实际上就是靠着注解处理器,在编译的过程来替我们完成这些复杂代码的编写的。

语法糖

语法糖分析包含泛型、自动装箱拆箱分析。

泛型

在java中,List<String>List<Integer>本质上是同一种类型——List,而在C#中则是两种类型。这是java为了能够兼容以前的代码,不得不进行的一种妥协,除了实现起来方便(几乎只需要修改部分编译器的内容)以外,其它在性能、灵活性等均被完爆……

接下来看看泛型是如何实现的。其实只需要将代码编译一下,然后反编译一下就知道了。

emmmm 我使用IDEA进行反编译发现反编译回来的代码仍然带着泛型……可能是太智能了吧。

总得来说就是会把List<String>类型擦除,变成List,然后每次放入取出的时候进行强制类型转化。

接下来谈谈java泛型的缺点:

  1. 不支持原生类型:List<int> list = new ArrayList<>(); //报错,这是因为int没办法转成Object。
  2. 由于编译器就自动把泛型擦掉了,那么运行的时候自然无法得知。但是由于有Signature参数,实际我们还是能够通过反射的方法获取到类型的。
  3. 针对重载的问题。部分编译器上可能会通过下面的代码(我的无法通过,确实是应该通过不了的):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class GenericTest {
public static String method(ArrayList<String> list){
System.out.println("method1");
return "";
}

public static int method(ArrayList<Integer> list){
System.out.println("method2");
return 1;
}
public static void main(String[] args) {
method(new ArrayList<String>());
}
}

自动装箱、拆箱和增强for循环

自动装箱和拆箱我想一般人都非常好理解,只需要等价替换即可。比如Integer i = 10;会被编译器自动装箱成Integer i = Integer.valueOf(10);

增强for循环也很好理解,本质上其实就是使用了目标对象的iterator来进行的遍历而已。

接下来是面试中可能也会问道的一段代码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AutoBoxTest {
public 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;
System.out.println(c == d); // true JVM会缓存-128~127
System.out.println(e == f); // false 同第一条
System.out.println(c == (a + b)); // true == 不发生自动拆箱,除非遇到运算符
System.out.println(c.equals(a + b)); // true 类型相同,值也相同
System.out.println(g == (a + b)); // true == 不发生自动拆箱,除非遇到运算符
System.out.println(g.equals(a + b)); // false 类型不同,值相同
}
}

条件编译

当java遇到if(true)这样子的代码的时候,会自动把else里面相关的代码给去掉,因为反正也不可能会被运行到。

后端编译

这里讲的后端编译就是讲的JIT及时编译器。

以Hotspot虚拟机为例,其内部同时有解释器与编译器,两者同时运行,且两者各有优缺点。

image-20200615214807004

mixed mode即说明运行的时候既用解释器,也用即时编译器。

解释器的存在可以让程序无需编译直接启动;编译器的存在可以让一些热点代码被编译成机器码从而提升java的运行效率。而且编译器还有三个,分别是客户端编译器(C1)、服务端编译器(C2)和实验性质的Graal编译器。

编译对象和条件

那么到底满足什么条件会触发编译器呢?编译哪些东西呢?

  • 被多次调用的方法。
  • 被多次执行的循环体。

而循环体只可能是在方法中的,所以其实面向的就是方法来进行即时编译。虚拟机同时还通过对方法栈进行周期性检查,如果发现某个方法经常在栈顶,则说明是热点方法;或者是通过一个计数器,当超过计数器的数值的就认为是热点方法,当然计数器的开销会比较大,因为每个方法都需要维护一个计数器。

当确定需要进行即时编译的时候,并不是直接交给编译器然后进行等待,而是交给编译器后继续由解释器进行执行,当下次再执行到的时候就可以用优化过的机器码来加速了。

提前编译器

基本从来没接触过的东西,类似于C++的那种编译器,全部给编译好。听上去很不错,这样效率不是大大提升了么?但是这么做其实是和java的目标不太一致,java本来就是为了跨平台而考虑的,而提前编译会要求一些物理机的信息来进行优化。

不过我们还是可以针对我们最常用的一些库来进行提前编译,这样能够在自己机器上测试的时候极大的提速。