Java“编译期”是一段“不确定”的操作过程:可能是指一个前端编译器(编译器的前端)把*.java文件转变为*.class文件的过程;可能是指虚拟机的后端运行期编译器(JIT编译器,Just In Time Compiler)把字节码转变为机器码的过程;可能是指使用静态提前编译器(AOT编译器,Ahead Of Time Compiler)直接把*.java文件编译成本地机器代码的过程。这三类编译过程中一些比较有代表性的编译器:

前端编译期:Sun的Javac/EclipseJDT中的增量式编译器(ECJ)。

JIT编译器:HotSpot VM的C1/C2编译器。

AOT编译器:GNU Compiler for the Java(GCJ)/Excelsior JET。

Javac这类编译器对代码的运行效率几乎没有任何优化措施。虚拟机设计团队把对性能的优化集中到了后端的即时编译器中,可以让那些不是由Javac产生的Class文件也能享受到编译器优化所带来的好处。但Javac做了许多针对编码过程的优化措施来改变程序员的编码风格和提高编码效率。相当多新生的Java愈发特性,都是靠编译器的“语法糖”来实现,而不是依赖虚拟机的底层改进来支持。Java中即时编译器在运行时期的优化过程对于程序运行更重要,而前端编译器在编译期的优化过程对于程序编码关系更加密切。

Javac编译器

Javc编译器是由Java编写的程序。

Javac的源码与调式

从Sun Javac的代码看来,编译过程大致可以分为三个过程:

解析与填充符号表过程。

插入式注解处理器的注解处理过程。

分析与字节码生成过程。

Javac编译动作的入口是com.sun.tools.javac.main.JavaCompiler类,上述三个过程的代码逻辑集中在这个类的compile()和compile2()里,整个编译最关键的处理由8个方法完成。

解析与填充符号表

解析步骤由图中的parseFiles()完成,解析步骤包括了经典程序编译原理中的词法分析和语法分析。

1 词法/语法分析

词法分析是将源代码的字符流转变为标记(Token)集合,单个字符时程序编写过程的最小元素,而标记则是编译过程的最小元素,关键字/变量名/字面量和运算符都可以成为标记。在Javac的源码中,词法分析由com.sun.tools.javac.parser.Scanner类实现。

语法分析是根据Token序列来构造抽象语法树的过程,抽象语法树(AST,Abstract Syntax Tree)是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表着程序代码中的一个语法结构(Construct),例如包/类型/修饰符/运算符/接口/返回值甚至连代码注视等都可以是一个语法结构。

在Javac源码中,语法分析由com.sun.tools.javac.parser.Parser类实现,这个阶段产生的抽象语法树由ccom.sun.tools.javac.tree.JCTree类表示,经过这个步骤后,编译器就基本不会再对源码文件进行操作了,后续操作都建立在抽象语法树上。

2 填充符号表

由enterTrees()完成。符号表(Symbol Table)由一组符号地址和符号信息构成的表格。符号表中登记的信息在编译的不同阶段都要用到。在语义分析中,符号表登记的内容将用于语义检查(如检查一个名字的使用和原先的说明是否一致)和产生中间代码。在目标代码生成阶段,当对符号名进行地址分配时,符号表示地址分配的依据。

由com.sun.tools.javac.comp.Enter类实现,此过程的出口是一个待处理列表(To Do List),包含了每一个编译单元的抽象语法树的顶级节点,以及package-info.java(如果存在的话)的顶级节点。

注解处理器

在JDK1.6中实现了JSR-269规范,提供了一组插入式注解处理器的标准API在编译期对注解进行处理,我们可以把它看做是一组编译器的插件,在这些插件里面,可以读取/修改/添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行了修改,那么编译器将回到解析及填充符号表的过程重新处理,直到所有的插入式注解处理器都没有再对语法树进行修改为止,每一次循环称为一个Round。

语义分析与字节码生成

语法树能表示一个结构正确的源程序的抽象,但无法保证源程序是符合逻辑的。而语义分析的主要任务是对结构上正确的源程序进行上下文有关性质的审查,如进行类型审查。

1 标注检查

Javac编译过程,语义分析过程为标注检查和数据及控制流分析两个步骤,分别由attribute()和flow()完成。

标注检查步骤检查的内容包括变量使用前是由已经声明/变量与赋值之间的数据类型是否匹配等。还有一个重要的动作称为常量折叠,如果我们在代码中:

int a = 1 + 2;

在语法树上仍然看到字面量“1”/“2”和操作符“+”号,但是经过常量折叠后,它们将会被折叠为字面量“3”。由于编译期间进行了常量折叠,所以在代码中“a=1+2”比起直接定义“a=3”,并不会增加程序运行期哪怕仅仅一个CPU指令的运算量。

2 数据及控制流分析

是对程序上下文逻辑更进一步的验证,它可以检查出程序局部变量在使用前是否赋值/方法的每条路径是否都有返回值/是否所有的受检查异常都被正确处理等问题。编译时期的数据及控制流分析与类加载时数据及控制流分析的目的基本上一致,但校验范围有所区别,有一些校验项只有在编译期或运行期才能进行。

//方法一带有final修饰符

public void foo(final int arg){

  final int var = 0;

}

//方法二没有final修饰

public void foo(int arg){

  int var = 0;

}

这两段代码编译出来的Class文件没有任何区别。将局部变量声明为final,对运行期没有任何影响,变量的不变性由编译器在编译期间保障。

3. 解语法糖

语法糖(Syntactic Sugar),指在计算机语言中添加某种语法,这种语法对语言的功能并没有影响,但是方便程序员使用。通常来说,使用语法糖能增加程序的可读性,从而减少代码出错的机会。

Java中最常用的语法糖主要是泛型、变长参数、自动装箱/拆箱等,虚拟机运行时不支持这些语法,它们在编译阶段还原回简单的基础语法结构,这个过程称为解语法糖。

4.字节码生成

字节码生成阶段不仅仅把前面各个步骤生成的信息(语法树、符号表)转换成字节码写到磁盘中,编译器还进行了少量代码添加和转换工作。

例如,实例构造器<init>()方法和类构造器<cinit>()方法就是在这个阶段添加到语法树中(这里的实例构造器不是指默认构造函数,如果代码中没有提供任何构造函数,那编译器将会添加一个没有参数的、访问性(public、protected或private)与当前类一致的默认构造函数,这个工作在填充符号表阶段已经完成),这两个构造器的产生过程实际上是一个代码收敛的过程,编译器会把语句块(对于实例构造器是{}块,对于类构造器是static块)、变量初始化(实例变量和类变量)、调用父类的实例构造器等操作收敛到<init>()和<cinit>()中,并保证一定先执行父类的实例构造器,然后初始化变量,最后执行语句块的顺序进行。除了生成构造器外,还有其他一些代码替换工作用于优化程序的实现逻辑,如把字符串的加操作替换为StringBuilder(大于等于JDK1.5)的append()操作。

Java语法糖的味道

泛型与类型擦除

泛型是JDK1.5新增特性,它的本质是参数化类型(Parametersized Type)的应用,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别为泛型类、泛型接口和泛型方法。

Java中泛型只在程序源码中存在,在编译后的字节码文件中,就已经替换为原生类型(Raw Type,也称裸类型),并在相应的地方插入了强制转型代码,因此,对于运行期的Java来说,ArrayList<Integer>与ArrayList<String>就是同一类,所以泛型技术实际上是Java的一颗语法糖,Java中泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。

//泛型擦除前
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("java", "hello java");
map.put("jasvascript", "hello javascript");
System.out.println(map.get("java"));
System.out.println(map.get("jasvascript"));
}
//泛型擦除后
public static void main(String[] args) {
Map map = new HashMap();
map.put("java", "hello java");
map.put("jasvascript", "hello javascript");
System.out.println((String)map.get("java"));
System.out.println((String)map.get("jasvascript"));
}

自动装箱、拆箱与遍历循环

//自动装箱、拆箱与遍历循环
public static void main(String[] args) {
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);
}

遍历循环需要被遍历的类实现Iterable接口。

条件编译

Java可以进行条件编译,方法是使用条件为常量的if语句。

//Java语言中的条件编译
public static void main(String[] args) {
if(true){
System.out.println("block 1");
}else{
System.out.println("clock 2");
}
} //上段代码在编译阶段被“运行”,生成一下代码
public static void main(String[] args) {
System.out.println("block 1");
}

只有使用条件为常量的if语句才能达到上述效果,如果使用常量与其他带条件判断能力的语句搭配,则可能在控制流分析中提示错误,被拒绝编译。

public static void main(String[] args) {
//编译器将会提示“Unreachable code”,拒绝编译
while(false){
System.out.println("");
}
}

深入理解JVM - 早期(编译期)优化的更多相关文章

  1. 《深入理解Java虚拟机》-----第10章 程序编译与代码优化-早期(编译期)优化

    概述 Java语言的“编译期”其实是一段“不确定”的操作过程,因为它可能是指一个前端编译器(其实叫“编译器的前端”更准确一些)把*.java文件转变成*.class文件的过程;也可能是指虚拟机的后端运 ...

  2. JVM笔记——编译期的优化

    一.编译过程 解析和填充符号表的过程 插入注解处理器的注解处理过程 语义分析与字节码生成过程 二.解析和填充符号表 解析包含两个过程:词法分析和语法分析 (一)词法分析 将源代码的字符流转变成标记(T ...

  3. 深入了解JVM虚拟机8:Java的编译期优化与运行期优化

    java编译期优化 java语言的编译期其实是一段不确定的操作过程,因为它可以分为三类编译过程:1.前端编译:把.java文件转变为.class文件2.后端编译:把字节码转变为机器码3.静态提前编译: ...

  4. 【深入理解JAVA虚拟机】第4部分.程序编译与代码优化.1.编译期优化。这章编译和实战部分没理解通,以后再看。

    1.概述 1.1.编译器的分类 前端编译器:Sun的Javac. Eclipse JDT中的增量式编译器(ECJ)[1].  把*.java文件转变成*.class文件 JIT编译器:HotSpot ...

  5. java编译期优化

    java语言的编译期其实是一段不确定的操作过程,因为它可以分为三类编译过程: 1.前端编译:把.java文件转变为.class文件 2.后端编译:把字节码转变为机器码 3.静态提前编译:直接把*.ja ...

  6. java编译期优化与执行期优化技术浅析

    java语言的"编译期"是一段不确定的过程.由于它可能指的是前端编译器把java文件转变成class字节码文件的过程,也可能指的是虚拟机后端执行期间编译器(JIT)把字节码转变成机 ...

  7. 数值类型中JDk的编译期检查和编译期优化

    byte b1 = 5;//编译期检查,判断是否在byte范围内 byte b2 = 5+4;//编译期优化,相当于b2=9 byte b3 = 127;//编译通过,在byte范围内 byte b4 ...

  8. JavaSe: String的编译期优化

    Java的编译期优化 因为工作的原因,经常会在没有源码的情况下,对一些产品的代码进行阅读.有时在解决Bug时,在运行环境下会直接去看class文件的字节码,来确定运行中版本是否正确的. 在看字节码时, ...

  9. jvm虚拟机笔记<五> 编译期优化

    JVM的编译器可以分为三个编译器: 1.前端编译器:把.java转变为.class的过程.如Sun的Javac.Eclipse JDT中的增量式编译器(ECJ). 2.JIT编译器:把字节码转变为机器 ...

随机推荐

  1. Keepalived + MySQLfailover + GTIDs 高可用

    架构图     10.1.1.207    mysql master + keepalived     10.1.1.206    mysql slave ( backup master ) + ke ...

  2. lua table库

      整理自:http://www.cnblogs.com/whiteyun/archive/2009/08/10/1543139.html 1.table.concat(table, sep,  st ...

  3. C++11并发学习之三:线程同步(转载)

    C++11并发学习之三:线程同步 1.<mutex> 头文件介绍 Mutex又称互斥量,C++ 11中与 Mutex 相关的类(包括锁类型)和函数都声明在 <mutex> 头文 ...

  4. 尽管是一个CS专业的学生,小B的数学基础很好并对数值计算有着特别的兴趣,喜欢用计算机程序来解决数学问题。现在,她正在玩一个数值变换的游戏。她发现计算机中经常用不同的进制表示同一个数,如十进制数123表达为16进制时只包含两位数7、11(B),用八进制表示时为三位数1、7、3。按不同进制表达时,各个位数的和也不同,如上述例子中十六进制和八进制中各位数的和分别是18和11。

    include "stdafx.h" #include<iostream> #include<vector> #include <algorithm& ...

  5. java面试的那些事

    跳槽面临的第一个难关那就是面试吧.面试的好坏直接关乎着你年薪的多少.如何顺利完成面试的那些难题,今天我们就从java中复习一下.看看经常面试的知识点,为什么面试这些知识点, 如果你是初级的或刚毕业的j ...

  6. 【转】使用 Python Mock 类进行单元测试

    出处:https://www.oschina.net/translate/unit-testing-with-the-python-mock-class?lang=chs&page=2#

  7. mysql-proxy做客户端连接转发【外网访问内网mysql】

    功能 用于外网客户端连接内网的MySQL,将此工具安装在中转服务器上. 软件版本 mysql-proxy-0.8.1-linux-rhel5-x86-64bit.tar.gz 简单的配置过程 解压后有 ...

  8. python函数式编程-------python2.7教程学习【廖雪峰版】(五)

    2017年6月13日19:08:13 任务: 看完函数式编程 笔记: 该看:函数式编程1.函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解 ...

  9. B. Worms Codeforces Round #271 (div2)

    B. Worms time limit per test 1 second memory limit per test 256 megabytes input standard input outpu ...

  10. spring;maven;github;ssm;分层;timestamp;mvn;

    [说明]本来还想今天可以基本搭建一个合适的ssm环境呢,结果发现,,太特么复杂了,网上的例子有好多,看了好多,下面的评论或多或少都有说自己运行产生问题的,搞的我也不敢好好下载运行 [说明]没办法,将目 ...