深入理解Java虚拟机(六)程序编译与代码优化

编译器优化

1、概述

代表性编译器:
1. 前端编译器:Sun的Javac、Eclipse JDT中的增量式编译器(ECJ)
2. JIT编译器:HotSpot VM的C1、C2编译器。
3. AOT编译器:GUN Compiler for the Java (GCJ)、Excelsior JET。

编译器的前端:把*.java文件转变成*.class的过程。
后端运行编译器:把字节码转变成机器码的过程。
静态提取编译器,直接把*.java文件编译成本地机器代码的过程。

2、Javac编译器

2.1、Javac的源码存放在JDK_SRC_HOME/langtools/src/share/classes/com/sun/tools/javac中,除了JDK自身的API外,就只引用了JDK_SRC_HOME/langtools/src/share/classes/com/sun/*里面的代码,调试环境建立起来简单方便。
编译过程:
1. 解析与填充符号表过程。
2. 插入式注解处理器的注解处理过程。
3. 分析与字节码生成过程。

2.2、解析与填充符号表
词法分析是将源代码的字符流转变为标记(Token)集合,单个字符是程序编写过程的最小元素,而标记则是编译过程的最小元素,关键字、变量名、字面量、运算符都可以成为标记。在Javac的源码中,词法分析过程由com.sun.tools.javac.parser.Scanner类来实现。
语法分析是根据Token序列构造抽象语法树的过程,抽象语法树是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表这程序代码中的一个语法结构,例如包、类、修饰符、运算符、接口、返回值、代码至少等都可以是一个语法结构。
完成了语法分析和词法分析之后就是填充符号表的过程,符号表是由一组符号地址和符号信息构成的表格,符号表所登记的内容将用于语义检查和产生中间代码。在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的依据。
在Javac源代码中,填充符号表的过程由com.sun.tools.javac.comp.Enter类实现,此过程的出口是一个待处理列表,包含了每一个编译单元的抽象语法树的顶级节点以及package-info.java的顶级节点。

2.3、注解处理器
注解与普通Java代码一样,是在运行期间发挥作用。在Javac源码中,插入式注解处理器的初始化过程试试在initPorcessAnnotations()方法中完成的,而它的执行过程则是在在processAnnotations()方法中完成的,这个方法判断是否还由新的注解处理器需要执行,如果有的话,通过com.sun.tools.javac.processing.JavacProcessingEnvironment类的的doProcessing()方法生成一个新的JavaCompiler对象对编译的后续步骤进行处理。

2.4、语义分析与字节码生成
1. 标注检查,检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配等。在标注检查步骤中,还有一个重要的动作称为常量折叠。标注检查步骤在Javac源码中的实现类是com.sun.tools.javac.comp.Attr类和com.sun.tools.javac.comp.Check类。
2. 数据及控制流分析
数据及控制流分析是对对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理了等问题。
3. 语法糖(Syntactic Sugar),也称糖衣语法,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来讲,使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。
在编译阶段还原简单的基础语法结构,这个过程称为解语法糖。在Javac的源码中,解语法糖的过程由desugar()方法触发,在com.sun.tools.javac.comp.TransTypes类和com.sun.tools.javac.comp.Lower类中完成。
4. 字节码生成
字节码生成是Javac编译过程的最后一个阶段,在Javac源码里面由com.sun.tools.javac.jvm.Gen类来完成。字节码生成阶段不仅仅是把前面各个步骤所生成的信息(语法树、符号表)转化成字节码写到磁盘中,编译器还进行了少量的代码添加和转换工作。

3、Java语法糖

3.1、泛型与类型擦除
泛型的本质是参数化类型(Parametersized Type)的应用,也就是说所操作的数据类型被指定为一个参数。Java中的泛型只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型,并且在相应的地方插入了强制转型代码,所以泛型技术实际上是Java的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。
3.2、自动装箱、拆箱与遍历循环

3.3、条件编译

运行期优化

1、概述

当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这写代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(JIT编译器)。

2、HotSpot虚拟机内的即时编译器

2.1、解释器与编译器
解释器与编译器两者各有优势:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。当程序运行环境中内存资源限制较大,可以使用解释器执行节约内存,反之可以使用编译执行来提升效率。同时,解释器还可以作为编译器激进优化时的一个“逃生门”,让编译器根据效率选择一些大多数时候都能提升运行速度的优化手段。

HotSpot虚拟机中内置了两个即时编译器,分别称为Client Compiler和Server Compiler,或者简称为C1编译器和C2编译器(也叫Opto编译器)。使用参数“-Xint”强制虚拟机运行于“解释模式”(Interpreted Mode),这时编译器完全不介入工作,全部代码使用解释方式执行。使用参数“-Xcomp”强制虚拟机运行于“编译模式”(Compiled Mode),这时将优先采用编译方式执行程序,但是编译器仍然要在编译无法进行的情况下介入执行过程,可以通过虚拟机的“-version”命令的输出结果显示这3种模式。

编译层次:
第0层,程序解释执行,解释器不开启性能监控功能(Profiling),可触发第1层编译。
第1层,也称为C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要将加入性能监控的逻辑。
第2层,也称为C2编译,也是将字节码编译为本地代码,但是会启动。

2.2、编译对象与触发条件
热点探测判定方式两种:
– 基于采样的热点探测:采用这种方法的虚拟机会周期性的检查各个线程的栈顶,如果发现某个或某些方法经常出现在栈顶,那这个方法就是“热点方法”。基于采样的热点探测的好处是实现简单、高效,还可以很容易的获取方法调用关系(将调用堆栈展开即可),缺点是很难精确的确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。
– 基于计数器的热点探测:采用这种方法的虚拟机会为每一个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阀值就认为它是“热点方法”。这种统计方法实现起来麻烦一些,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系,但是它的统计结果相对来说更加精确和严谨。在HotSpot虚拟机中使用的这种,为每个方法准备了两类计数器:方法调用计数器和回边计数器。

2.3、编译过程
在第一个阶段,一个平台独立的前端将字节码构造成一种高级中间代码表示(High-Level Intermediate Representaion,HIR)。
在第二个阶段,一个平台相关的后端从HIR中产生低级中间代码表示(Low-Level Intermediate Representation,LIR),而在此之前会在HIR上完成另外一些优化。
最后阶段是在平台相关的后端使用线性扫描算法(Linear Scan Register Allocation)在LIR上分配寄存器,并在LIR上做窥孔(Peephole)优化,然后产生机器代码。

2.4、查看及分析即时编译结果
使用参数-XX:+PrintCompilation要求虚拟机在即时编译时将被编译成本地代码的方法名称打印出来。

    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/shuaicihai/article/details/78268250
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞