JVM-1.编译

时间:2021-12-18 05:30:21
目录
一、编译器概述
二、编译器组成
三、示例
四、深入理解JVM中的编译器
五、语法糖
六、补充
 
 
一、编译器概述
1、编译器实质
编译器的实质是将一种语言规范转化为另一种语言规范;由人容易理解到机器容易理解。Javac将java文件中的源代码,转化成JVM能够识别的class文件中的字节码;然后JVM再转化为机器可以识别的机器语言。
2、编译器类型
前端编译器:.java->.class;如Sun的javac
JIT编译器:运行期编译器,字节码->机器码;如Hotspot的C1、C2编译器
AOT编译器:静态提前编译器,.java->机器码
3、前端编译器对代码的优化主要在程序编码(帮助程序员改善编码风格、提高编码效率);而JIT主要是性能优化,这样其他语言的代码也可以被优化。
许多Java新的语法特性,是通过语法糖实现,而不需要虚拟机的底层支持。
 
 
二、编译器组成
1、词法分析器:生成token流。词法分析器读取源文件中的每个字符,识别关键字、分隔符等,生成token流;token流中包括:
(1)java关键词:package、import、public、class、int等
(2)自定义单词:包名、类名、变量名、方法名
(3)符号:=、;、+、-、*、/、%、{、}等
2、语法分析器:生成结构化的、可操作的语法树;其实是将token中的单词组成一句句话。有些地方叫做抽象语法树(AST)。
3、语义分析器:生成注解语法树,更接近目标语言的语法规则。语义分析器在语法树基础上做很多处理,包括
1、添加默认的无参构造器(在没有指定任何有参构造器的情况下)【《JVM》中,在语法分析器和注解处理之前,有一步填充符号表,其中的操作包括添加默认的无参构造器】
2、处理注解:JDK1.6以后,开始提供插入式注解处理器的API,可以理解为编译器的插件;如果注解处理器修改了语法树,编译器将循环进行(解析+填充+注解处理);通过注解处理器,程序员可以干预编译过程。
3、标注:检查语义合法性、进行逻辑判断
•检查语法树中的变量类型是否匹配(eg.String s = 1 + 2;//这样"="两端的类型就不匹配)
•检查变量、方法或者类的访问是否合法(eg.一个类无法访问另一个类的private方法)
•变量在使用前是否已经声明、是否初始化
•常量折叠(eg.代码中:String s = "hello" + "world",语义分析后String s = "helloworld")
•推导泛型方法的参数类型
4、数据及控制流分析【编译时分析与类加载时分析,目的类似,但校验范围有一些区别。如局部变量在内存中没有flag,对final的校验只能在编译阶段进行】
•局部变量使用前是否有赋值【《JavaWeb》认为属于上一步,《JVM》认为属于本步】
•变量的确定性赋值(eg.有返回值的方法必须确定有返回值)
•final变量只能赋一次值,在编译的时候再赋值的话会报错
•所有的检查型异常是否抛出或捕获
•所有的语句都要被执行到(return后边的语句就不会被执行到,除了finally块儿)
5、进一步语义分析
•去掉永假代码(eg.if(false))
•变量自动转换(eg.int和Integer)
•去掉语法糖(eg.foreach转化为for循环,assert转化为if,内部类解析成一个与外部类相关联的外部类)
6、最后,将经过上述处理的语法树转化为最后的注解语法树
【个人记忆】除了注解处理、去除语法糖,可以这么分类:
类:添加无参构造器;变量、方法、类的访问是否合法
字段:变量类型是否匹配;final是否多次赋值;常量折叠
方法外部:返回值是否正确;异常是否抛出或捕获
方法内部:变量使用前是否声明并初始化;所有语句都被执行到;去除永假代码
4、代码生成器:生成字节码
(1)生成<init>()和<cinit>()方法:其实是代码收敛,包括{}、static{}、变量初始化、调用父类的实例构造器等。
(2)其他优化:如字符串的加操作替换为StringBuilder或StringBuffer的append()操作。
(3)生成字节码
 
 
三、示例
(1)源代码到token
package compile;

/**
* 词法
*/
public class Cifa {
int a;
int c = a + 1;
}

JVM-1.编译

(2)语法树
package compile;
public class Yufa {
    int a;
    private int c = a + 1;
    public int getC() {
        return c;
    }
    public void setC(int c) {
        this.c = c;
    }
}

JVM-1.编译

 
 
四、深入理解JVM中的编译器
1、《JVM》一书中对编译器的编译过程的划分不太一样,但大体一致。
2、解析与填充符号表
解析:词法分析和语法分析
填充符号表:符号表是一组符号地址和符号信息构成的表格;符号表中信息在编译阶段多次用到。如语义分析时用于语义检查;字节码生成阶段,当对符号名分配地址时,符号表是地址分配的依据。
3、注解处理
4、语义分析
标注检查
数据及控制流分析
解语法糖
5、字节码生成
 
 
五、语法糖
1、语法糖:在计算机语言中添加某种语法,这种语法能使程序员更方便的使用语言开发程序,同时增强程序代码的可读性,避免出错的机会;但是这种语法对语言的功能并没有影响。Java中的泛型,变长参数,自动拆箱/装箱,foreach循环,条件编译,枚举,内部类,断言,对枚举和字符串的switch,try语句中定义和关闭资源(?)等都是语法糖,即虚拟机本身并不支持。
2、泛型:伪泛型,类型擦除
泛型的本质是参数化类型;泛型出现前常用方法是Object类+类型转换;Java泛型的实现是类型擦除,即伪泛型。
例如List<String>和List<Integer>在运行期是一个类,即List;使用时进行类型转换。
泛型在重载方面的尴尬:下面两个方法无法重载。
public void method(List<Integer> list){}
public void method(List<String> list){}
由于类型被擦除,为了区分不同参数化类型,引入了一些其他属性,如Signature等;因此类型擦除只是对方法的Code属性中的字节码进行擦除,元数据仍保留了泛型信息,因此可以通过反射手段取得参数化类型。【这段理解不深入,略】
3、自动拆箱和自动装箱:intValue()和Integer.valueOf()
变长参数:参数是数组
foreach循环:iterator
4、条件编译
C、C++ 的预处理器最初的任务是解决编译时的代码依赖关系(如#include),而在 Java 语言之中并没有使用预处理器,因为 Java 语言天然的编译方式(不一个个编译 Java 文件,而是将所有编译单元的语法树*结点输入到待处理列表后再进行编译,因此各个文件之间能够互相提供符号信息) 无需使用预处理器。
实现举例:
public static void main(String[] args) {
    if(true){
        System.out.println("True");
    }else{
        System.out.println("False");
    }
}
编译后:
public static void main(String args[])
{
    System.out.println("True");
}


注意:只有if+常量可以实现条件编译,常量+while等编译会报错;消除无法到达的分支也是在解语法糖阶段完成;只能实现语句块级别的条件编译。
 
 
六、补充
1、实战:插入式注解处理器,略
2、访问者设计模式:将数据结构(如语法树)和对数据结构的操作解耦;略。
3、主要参考:《深入理解Java虚拟机》