标签:
一个*.java文件总体要经过编译期和运行期,会涉及到两类编译期:
①编译期编译:一般表示*.java->*.class(包含字节码)的过程 — 也叫前端编译。
②运行期编译:一般表示*.class->机器码的过程 — 也叫后端编译。
■前端编译器
●作用:把*.java->*.class,以供加载器进行类型加载,并在在编译期优化程序编码。
●种类:Sun的Javac、Eclipse的JDT。
■后端编译器(JIT编译器)
●作用:把*.class->机器码,以供解释器解释执行,并在在运行期优化程序运行。Just In Time Compiler,专指在VM运行的编译器。
●种类:HotSpot VM中的C1、C2编译器。
■静态提前编译器(AOT编译器)
●作用:直接把*.java文件编译成本地机器代码。Ahead Of Time Compiler。
Javac是Sun公司,使用Java语言开发的Java Compiler,只针对*.java文件,将其编译成*.class文件。Javac很具有代表性,所以以Javac为例讲解整个前端编译过程。
注:其他语言编写的源代码文件,也是可以由特定的编译器编译成*.class文件的。如JRuby、Groovy语言编写的源代码文件,经编译器编译成*.class文件后,亦可以在JVM上运行。
由于Javac由Java编写,所以可以通过查看它的源码了解其编译*.java的逻辑过程。从Javac的源码中可梳理出编译的过程,大致分为三大过程:①解析与填充符号表;②注解器的注解处理;③语义分析和字节码生成。整个过程主要由其API中的compile()和compile2()方法来完成。逻辑源码如下:
注解处理------又返回到------>解析与填充符号表的解释:因为在注解处理过程中,有可能对语法树进行了修改,所以要回到“解析与填充符号表”过程重新处理,直到没有对语法树的修改为止。
解析包括:词法分析和语法分析,在parseFiles()方法中完成。
■词法分析:将源代码中的字符流转变为标记(Token)集合的过程。
■语法分析:根据Token序列构造*.java的抽象语法树的过程。
●抽象语法树(SAT-Abstract Syntax Tree):①用来描述程序代码语法结构的树形表达方式;②表示一个源代码文件结构正确的抽象。
●抽象语法树的结构视图如下:
注:经过解析过程后,编译器就基本不再对*.java文件进行操作了,后续的操作都是以其抽象语法树为基础进行的。
完成词法、语法分析后,接着就是填充符号表,在enterTrees()方法中完成。
■符号表:由一组符号地址和符号信息构成的表格。
■作用:该表可用于编译期的不同阶段。
●语义分析阶段:语义检查和中间代码生成。
●字节码生成阶段:对符号名进行地址分配时,是地址分配的依据。
■比如:在该阶段默认构造方法的添加。
如果代码中没有提供任何构造方法,那么在该阶段编译器会添加一个无参的、访问类型(public、protected或private)和当前类一致的,默认构造方法。
注解器的初始化过程在initProcessAnnotations()方法中完成,执行过程在
processAnnotations()方法中完成。
■注解(Annotations):与Java代码一样,是在运行期间发挥作用的。
■在处理注解过程中,如果注解对抽象语法树进行了修改(比如重新添加了一些代码等修改),那么编译器会回到“解析与填充符号表”阶段。比如:在代码中使用注解处理器的情 况。
●注解被处理之前
1 public @Data class LombokPojoDemo { 2 private String name; 3 } 4 5 // from project lombok 6 @Target(ElementType.TYPE) 7 @Retention(RetentionPolicy.SOURCE) 8 public @interface Data { 9 String staticConstructor() 10 default ""; 11 }
●注解被处理之后
语法分析之后,编译器获得了一个*.java的抽象语法树,该语法树表示这个*.java的结构是正确的。但并不知道,其中的源码在逻辑上是否也是正确的。语义分析的任务就是对结构上正确的*.java进行逻辑上的检查(上下文有关性质的审查:如进行类型审查),具体的检查操作是在抽象语法树上完成的。比如:
后续出现的三种运算表达式,在Java语言的结构上都是正确的,但后两个表达式并不符合Java语言的逻辑,即在语义上是不正确的(注:在C语言中后两个表达式是符合C语言语义的)。
Javac的编译过程中,语义分析包括:①标注检查;②数据流与控制流分析。
■标注检查
●标注检查在attribute()方法中完成。
●标注检查的内容包括:①变量使用前是否已被声明;②变量与赋值之间的数据类型是否匹配;③常量折叠等。
●比如常量折叠:对于在编译期间就可确定值的常量,如果有“+”操作符,则无需等到程序运行起来后再进行“+”操作,而是在编译的语义分析阶段,编译器直接就确定常量“+”之 后的结果值。
◎比如数值类型常量的折叠:int a = 1 + 2;在语法树上仍能看到字面量“1”、“2”和操作符“+”,但经过标注检查中的常量折叠后,它们会被折叠为字面量“3”,标注在语法树 上。
注:由于在编译期进行了常量折叠,所以代码中定义的int a = 1 + 2;,并不会比定义a = 3;增加CPU的运算量。
◎比如String类型常量的折叠:
1 public class Test26{ 2 public static void main(String[] args){ 3 String a = "a9"; 4 String b = "a" + 9; 5 System.out.println(a == b); 6 } 7 }
运行结果是:true
编译后的Java Class文件如下:
说明:查看编译后的Java Class文件中的constant_pool,会发现其中只有一个字符串字面值”a9”,即是说"a" + 9在编译阶段就已经被优化折叠成了”a9”。
◎String类型的变量就没有所谓的折叠:
1 public class Test26{ 2 public static void main(String[] args){ 3 String a = "qinfen"; 4 String b = "qin"; 5 String c = b + “fen”; 6 System.out.println(a == c); 7 } 8 }
运行结果是:false
说明:“+”操作中,由于有字符串引用b的存在,而编译器是无法在编译期间确定b的值的,所以就无法进行常量折叠。“+”操作是在程序运行期间使用StringBuilder的append()方法进行替换的。
作为对比:
1 public class Test26{ 2 public static void main(String[] args){ 3 String a = "qinfen"; 4 final String b = "qin"; 5 String c = b + “fen”; 6 System.out.println(a == c); 7 } 8 }
运行结果是:true
编译后的Java Class文件如下:
说明:对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。所以此时的b + “fen”和"qin" + "fen"效果是一样的。
■数据流与控制流分析
●数据流以控制流分析在flow()方法中完成。
●它是对程序上下文逻辑更进一步的验证,包括:①局部变量在使用前是否已被赋值;②方法的每条路径是否都有返回值;③是否所有的受查异常都被正确处理了;④检查final修 饰的变量不被重复赋值等。
●编译期的数据流及控制流分析和类加载时的数据流及控制流分析的目的基本上一致。
语法糖:指在计算机语言添加的某种语法,对语言的功能没有影响,但是更方便程序员使用。由desugar()方法完成。
■语法糖有:泛型、变长变量、自动装箱拆箱、断言(assertion)、foreach循环、enum类型的switch、String类型的switch(Java 7)、具名内部类/匿名内部类/类字面量等 等。
■解语法糖:JVM运行时不支持语法糖,所以它们在编译阶段会被还原回简单的基础语法结构。
●比如:泛型
◎泛型:本质就是参数化类型,把操作的数据类型指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别成为泛型类、泛型接口和泛型方法。
◎Java语言中泛型只存在于源码中,在编译期会将其还原为原生类型。如果源码中有:ArrayList<int>和ArrayList<String>,在编译期会将它俩还原为:ArrayList和 ArrayList的原生类型。所以,对于运行期而言ArrayList<int>和ArrayList<String>是同一个类型。
●比如:削除if (false) { … }形式的无用代码
满足下述所有条件的代码被认为是条件编译的无用代码,会在该阶段被清除。
◎if语句的条件表达式是Java语言规范定义的常量表达式。
◎并且常量表达式的值为false则then块为无用代码;反之则else块为无用代码。
●比如:泛型+自动装箱拆箱
◎类型转换前(解语法糖前)
public void desugarGenericToRawAndCheckcastDemo() { List<Integer> list = Arrays.asList(1, 2, 3); list.add(4); int i = list.get(0); }
◎类型转换后(解语法糖前)
public void desugarGenericToRawAndCheckcastDemo() { List list = Arrays.asList(1, 2, 3); list.add(4); int i = (Integer)list.get(0); }
◎解语法糖后
●foreach循环
◎解语法糖前
public void desugarDemo() { Integer[] array = {1, 2, 3}; for (int i : array) { System.out.println(i); } assert array[0] == 1; }
◎解语法糖后
public void desugarDemo() { Integer[] array = { Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3) }; for (Integer[] arr$ = array, len$ = arr$.length, i$ = 0;i$ < len$; ++i$) { int i = arr$[i$].intValue(); { System.out.println(i); } } if (!$assertionsDisabled && !(array[0].intValue() == 1))throw new AssertionError(); }
该阶段的主要任务就是由编译器生成Java字节码,但在Java字节码生成之前,编译器
会在抽象语法树上进行一些代码的生成添加和转换操作。
■代码的添加和转换
●类型初始化方法<clinit>()和类实例化方法<init>()的生成添加
◎类型初始化方法<clinit>()的生成添加
在该阶段,如果源代码中含有:①静态变量的赋值语句;②或static{}代码块,编译器则会创建1个该类(或接口)的初始化方法<clinit>()(有且只有一个),并将静态变量 的赋值语句或static{}代码块收集到<clinit>()方法中。
注:有3种情况,使编译器在该阶段不会生成类初始化方法<clinit>():①类中没有声明静态变量,也没有static{}代码块;②类中声明了静态变量,但没有任何赋值语句;③类中仅包含编译时常量。
◎类实例化方法<init>()的生成添加
在该阶段,编译器会针对类中的每一个构造方法,对应生成一个<init>()方法。如果类中没有显式的声明任何构造方法,编译器还是会生成一个默认的无参构造方法(该构造 方法仅调用超类的无参构造方法),对应的还是会生成一个<init>()方法。
如果源代码中含有:①实例变量的赋值语句;②或{}代码块,编译器会将它们收集到<init>()方法中。所以构造方法对应的类实例化方法<init>()中通常会包含3种代码:① 另一个<init>()方法的调用;②收集到的实例变量赋值语句或{}代码块;③构造方法体中所有的代码语句。
■代码的转换(优化程序)
●把字符串的“+”操作,替换为StringBuffer或StringBuilder的append()操作。
●x++/x—在条件允许时被优化为++x/--x
最后,在完成对语法树的遍历(后序遍历)和调整之后,会把填充了所有所需信息的符号表交到com.sun.tools.javac.jvm.ClassWriter类手上,再有这个类的writeClass()方法输出字节码,生成最终的*.class文件。
标签:
原文地址:http://www.cnblogs.com/beikeer/p/4437746.html