JVM理论:(四/1)编译过程——早期(编译期)

Posted zjxiang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM理论:(四/1)编译过程——早期(编译期)相关的知识,希望对你有一定的参考价值。

  Java 语言的 “编译期”其实可以分为3类编译过程:

  前端编译器:把*.java文件转变成*.class文件的过程。

  后端运行期编译器(JIT编译器):把字节码转变成机器码的过程。

  静态提前编译器(AOT编译器):直接把*.java文件编译成本地机器代码的过程。

  Javac这类编译器对代码的运行效率几乎没有任何优化措施。虚拟机设计团队把对性能的优化集中到了后端的即时编译器中,这样可以让那些不是由Javac产生的Class文件也同样能享受到编译器优化所带来的好处(如JRuby、Groovy等语言的Class文件)。

一、早期(编译期)优化

Javac编译过程

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

1、解析与填充符合表过程。

  1.1、词法分析。将源代码的字符流转变为标记(Token)集合。标记是编译过程的最小元素,关键字、变量名、字面量、运算符都可以成为标记,如“int a = b + 2”这句代码包含了6个标记,分别是int、a、=、b、+、2,虽然关键字int由3个字符构成,但是它只是一个Token,不可再拆分。

  1.2、语法分析。根据Token序列构造抽象语法树的过程。抽象语法树是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个阶段都代表着程序代码中的一个语法结构,例如包、类型、修饰符、运算符、接口、返回值甚至代码注释等都可以是一个语法结构。经过这个步骤后,编译器就基本不会再对源码文件进行操作了,后续的操作都建立在抽象语法树之上。

  1.3、填充符合表。符号表是由一组符号地址和符号信息构成的表格,可以把它想象成哈希表中K-V值对的形式。符号表中所登记的信息在编译的不同阶段都要用到。在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的依据。

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

  在JDK1.5之后,Java语言提供了对注解(Annotation)的支持,这些注解与普通的Java代码一样,是在运行期间发挥作用的。

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

  技术分享图片

  有了编译器注解处理的标准API后,我们的代码才有可能干涉编译器的行为,由于语法树中的任意元素,甚至包括代码注释都可以在插件之中访问到,所以通过插入式注解处理器实现的插件在功能上有很大的发挥空间。

3、分析与字节码生成过程。

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

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

  3.3、字节码生成。是Javac编译过程的最后一个阶段,生成最终的Class文件,编译器还进行了少量的代码添加和转换工作。例如,实例构造器<init>()方法和类构造器<clinit>()方法就是在这个阶段添加到语法树之中的。

  注意,这里的实例构造器并不是指默认构造函数,如果用户代码中没有提供任何构造函数,那编译器将会添加一个没有参数的、访问性(public、protected或private)与当前类一致的默认构造函数。

  两个构造器的产生过程实际上是一个代码收敛的过程,编译器会把语句块(实例构造器是“{}”块,类构造器是“static{}”块)、变量初始化(实例变量和类变量)、调用父类的实例构造器(仅仅是实例构造器,<clinit>()方法中无须调用父类的<clinit>()方法,虚拟机会自动保证父类构造器<clinit>先执行)等操作收敛到<init>()和<clinit>()方法之中,并且保证一定是按先执行父类的实例构造器,然后初始化变量,最后执行语句块的顺序进行。

Java语法糖

  除了泛型、自动装箱、自动拆箱、遍历循环、变长参数和条件编译之外,Java语言还有不少其他的语法糖,如内部类、枚举类、断言语句、对枚举和字符串的switch支持、try语句中定义和关闭资源等。

1、泛型与类型擦除

   C#里面泛型无论在程序源码中、编译后,或是运行期中,都是切实存在的,List<int>与List<String>就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,基于这种方法实现的泛型称为真实泛型。

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

  Java的泛型在某些场景下确实存在不足。如在重载中:

/**
 * 当泛型遇到重载
 */
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<Integer>和List<String>编译之后都会被擦除了,变成了一样的原生类型List<E>,擦除动作导致这两种方法的特征签名变得一模一样。

  由于Java泛型的引入,各种场景(虚拟机解析、反射等)下可能都需要获取泛型类中的参数化类型。因此引入了诸如Signature、LocalVariableTypeTable等新的属性用于解决伴随泛型而来的参数类型的识别问题,Signature是其中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名,这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息。

 

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

/**
 * 自动装箱的陷阱
 */
public class Test {
    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    
        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
    }
}

  自动装箱其实就是编译器编译的时候,自动帮你把int变成Integer对象,调用的是Integer的静态方法valueOf()。

  这篇文章已经做了很详细的解析,https://blog.csdn.net/woshiwu/article/details/6637310。几个关键点。

  包装类的“==”运算在不遇到算术运算的情况下不会自动拆箱,它们的equals方法不处理数据转型的关系。

  ==两边是基本数据类型时,是比较值是否相等,是引用类型时,比较是否指向堆中同一个对象。
  系统已经把-128到127之间的Integer缓存到一个Integer数组中去了,返回的不是一个新new出来的Integer对象,而是一个已经缓存在堆中的Integer对象。

 

 3、条件编译

/**
 * Java语言的条件编译
 */
public static void main(String[] args) {
    if(true){
        System.out.println("block 1");
    }else{
        System.out.println("block 2");
    }
}

  上述生成的字节码之中只包括“System.out.println("block 1");”一条语句,并不会包含if语句及另外一个分子中的“System.out.println("block 2");”。 

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

 

https://blog.csdn.net/u013132035/article/details/78598979


以上是关于JVM理论:(四/1)编译过程——早期(编译期)的主要内容,如果未能解决你的问题,请参考以下文章

JVM-程序编译与代码早期(编译期)优化

JVM理论:(四/2)编译过程——晚期(运行期)

早期(编译期)优化

Javac早期(编译期)

程序编译与代码优化 -- 早期(编译期)优化

早期(编译期)优化