Java代码编译优化

»深入理解Java虚拟机读书笔记

目录:

Java代码的执行大致可以分为如下过程。

因此,java中的编译器可以分为三类:

这里主要讨论前两种编译器。

javac编译器

使用javac编译就是将.java文件编译成.class字节码文件的过程,javac的编译过程大致分为三步

  1. 解析与填充符号表过程
  2. 插入式注解处理器的注解处理过程
  3. 语义分析与字节码生成过程

解析与填充符号表过程

该过程主要有三步:

(1) 词法分析

该过程指的是将源代码的字符流转变为标记集合。

例如:int a = b + 2;

这段代码包含了6个标记,int,a,=,b,+,2

(2) 语法分析

语法分析指的是根据标记序列构造抽象语法树的过程

(3) 填充符号表过程

符号表是由一组符号地址和符号信息组成的表格,即可以认为是将符号信息和符号地址进行统计,便于后续的过程中获取符号

注解处理器

就是在编译期间对注解进行处理,通过注解处理器可以读取、修改、添加抽象语法树中的任意元素

语义分析与字节码生成过程

该过程主要有四步:

(1) 标注检查

标注检查主要是检查:

其中常量折叠指的是将常量进行计算 例如:

int a = 1 + 2;

编译之后会定义a = 3;因此,与int a = 3相比,运行速度是一样的。

(2) 数据及控制流分析

该过程是对程序上下文逻辑进行进一步的检查:

(3) 解语法糖

语法糖可以看作是编译器的小把戏,为了方便程序员编写代码,增加程序的可读性

例如:泛型机制

泛型的本质是参数化类型,就是将操作的数据类型指定为一个参数

在早前没有泛型的之后,程序员需要通过指定Object类型,然后强转来实现特定类型的数据的读取,这样只有运行的时候才能发现某些类型的错误。

而加入泛型之后可以让错误在编译时就被发现。但是在java中泛型只会存在与源码中,编译之后的字节码是不存在泛型的,也就是说字节码里面仍然是通过强转Object类型来实现。即运行时ArrayList与ArrayList是同一个类,里面都存放的是Object类型。这也就是类型擦出,即在编译的时候会将泛型的数据类型在字节码中擦出,变成一样的原生类型。

此外还有自动装箱、拆箱、foreach循环等都是语法糖

(4) 字节码生成

该过程将前面生成的信息转换成字节码写到磁盘,同时还会进行一些代码的替换优化,例如将字符串的相加替换为StringBuilder的append操作

即时编译器

早先,java程序是通过解释器进行解释执行。即直接解释字节码,从而运行程序。而后来,为了提高热点代码(运行特别频繁的代码)的执行效率,在运行时,虚拟机会将这部分热点代码编译成与本地平台相关的机器码,并进行各种优化,完成这个任务的编译器叫即使编译器(JIT)

即时编译器并不是虚拟机必备的,但是是衡量虚拟机性能的一个很重要的指标。

(1) 解释器与编译器

主流的虚拟机都采用解释器与编译器共存的架构。

当程序需要迅速启动和执行的时候,使用解释器更有优势,省去编译时间。在程序后台运行的时候,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码后,可以获得更高的执行效率。解释器与编译器通常都是配合使用。

HotSpot内置两个即时编译器,Client Compiler、Server Compiler简称C1、C2。通常使用解释器与其中一个编译器直接配合的方式工作。

为了在程序启动响应速度和运行效率之间达到最佳平衡,HotSopt采用分层策略。

实施分层编译后,C1、C2将会同时工作,许多代码会被多次编译,用C1获取更高的编译速度,C2获取更好的编译质量。

(2) 编译触发的条件

在运行时会被即时编译器编译的热点代码有两种:

对于后一种情况,尽管编译是由循环体触发,但编译器依然会以整个方法作为编译对象,这种编译方式发生在方法执行过程中,称之为“栈上替换”,即方法栈帧还在栈上,方法就被替换了。

判断一段代码是不是热点代码:

通常采用基于计数器的热点探测。

计数器分为:方法调用计数器、回边计数器