深刻理解Java虚拟机读书笔记-第10章 前端编译与优化

第10章 前端编译与优化

10.1 概述

先明确几个概念 即时编译器(JIT编译器,Just In Time Compiler),运行期把字节码变成本地代码的过程。 提早编译器(AOT编译器,Ahead Of Time Compiler),直接把程序编译成与目标及其指令集相关的二进制代码的过程。html

这里讨论的“前端编译器”,是指把*.java文件转换成*.class文件的过程,主要指的是javac编译器。前端

10.2 Javac编译器

10.2.1 介绍

Javac编译器是由Java语言编写。分析Javac代码的整体结构来看,编译过程大体分为1个准备过程和3个处理过程。以下:java

  • 1)准备过程:初始化插入式注解处理器
  • 2)解析与填充符号表
    • 词法、语法分析。将源代码的字符流转变为标记集合,构造出抽象语法树。
    • 填充符号表。产生符号地址和符号信息。
  • 3)插入式注解处理器的注解处理
  • 4)分析与字节码生成
    • 标注检查。对语法的静态信息进行检查。
    • 数据流及控制流分析。对程序动态运行过程进行检查
    • 解语法糖。将语法糖代码还原为原有形式
    • 字节码生成。将前面各个步骤生成的信息转化为字节码。

若是注解处理产生新的符号,又会再次进行解析填充过程。 截屏2020-08-28上午10.10.16.png Javac编译动做的入口com.sun.tools.javac.main.JavaCompiler类。代码逻辑主要所在方法compile(),compile2() 截屏2020-08-28上午11.24.33.png编程

10.2.2 解析和填充符号表

1. 词法、语法分析

对应parserFiles()方法 词法分析:源码字符流转变为标记(Token)集合的过程。标记是编译时的最小元素。关键字、变量名、字面量、运算符都是能够做为标记。 如“int a = b + 2”, 包含了6个标记,int,a , =, b, +, 2 。词法分析由com.sun.tools.javac.parser.Scanner实现。 语法分析:根据标记序列构造抽象语法树的过程。抽象语法树(Abstract Syntax Tree,AST),描述代码语法结构的树形表示形式,树的每一个节点都表明一个语法结构,例如包,类型,运算符,接口,返回值等等。com.sun.tools.javac.parser.Parser实现。抽象语法树是以com.sun.tools.javac.tree.JCTree类表示。 后续的操做创建在抽象语法树之上。数组

2.填充符号表

对应enterTree()方法。markdown

10.2.3 注解处理器

JDK6,JSR-269提案,“插入式注解处理器”API。提早至编译期对特定注解进行处理,能够理解成编译器插件,容许读取、修改、添加抽象语法树中的任意元素。若是产生改动,编译器将回到解析及填充符号表过程从新处理,直到不产生改动。每一次循环过程称为一个轮次(Round).
使用注解处理器能够作不少事情,譬如Lombok,能够经过注解自动生成getter/setter方法、空检查、产生equals()和hashCode()方法。
复制代码

10.2.4 语义分析与字节码生成

抽象语法树可以表示一个正确的源程序,但没法保证语义符合逻辑。语义分析的主要任务是进行类型检查、控制流检查、数据流检查等等。 例如oop

int a = 1;
boolean b = false;
char c = 2;

//后续可能出现的运算,都是能生成抽象语法树的,但只有第一条,能经过语义分析
int  d= a + c;
int  d= b + c;
char d= a + c;
复制代码

在IDE中看到的红线标注的错误提示,绝大部分来源于语义分析阶段的结果。优化

1. 标注检查

attribute()方法,检查变量使用前是否已被声明,变量与赋值的数据类型是否匹配等等。 3个变量的定义属于标注检查。标注检查顺便会进行极少许的一些优化,好比常量折叠(Constant Folding).ui

int a = 1 + 2; 实际会被折叠成字面量“3复制代码

2. 数据及控制流分析

flow()方法,上下文逻辑进一步验证,好比方法每条路径是否有返回值,数值操做类型是否合理等等。spa

3. 解语法糖

语法糖(Syntactic Sugar),编程术语 Peter J.Landin。减小代码量,增长程序可读性。好比Java语言中的泛型(其余语言的泛型不必定是语法糖实现,好比C#泛型直接有CLR支持),变长数组,自动装箱拆箱等等。 解语法糖,编译期将糖语法转换成原始的基础语法。

4. 字节码生成

  • 将前面生成的信息(语法树,符号表)转化为字节码,
  • 少许代码添加,(),()等等
  • 少许代码优化转换,字符串拼接操做替换为StringBuffer或StringBuilder等等。

10.3 Java语法糖

10.3.1 泛型

1.Java泛型

JDK5,Java的泛型实现称为“类型擦除式泛型”(Type Erasure Generic),相对的C#选择的是“具现化泛型”(Reified Generics),C#泛型不管在源码中,仍是编译后的中间语言表示(此时泛型都是一个占位符),List<int> 与List<String>是两个不一样的类型。而Java泛型,只是在源码中存在,编译后都变成了统一的类型,称之为类型擦除,在使用处会增长一个强制类型转换的指令。
复制代码
Map<String, String> stringMap = new HashMap<String, String>();
stringMap.put("hello", "你好");
System.out.println(stringMap.get("hello"));

Map objeMap = new HashMap();
objeMap.put("hello2", "你好2");
System.out.println((String)objeMap.get("hello2"));
复制代码

截取部分字节码

0: new           #2                  // class java/util/HashMap
4: invokespecial #3                  // Method java/util/HashMap."<init>":()V
13: invokeinterface #6,  3            // InterfaceMethod java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
25: invokeinterface #8,  2            // InterfaceMethod java/util/Map.get:(Ljava/lang/Object;)Ljava/lang/Object;
30: checkcast     #9                  // class java/lang/String
33: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 
        
36: new           #2                  // class java/util/HashMap
40: invokespecial #3                  // Method java/util/HashMap."<init>":()V
49: invokeinterface #6,  3            // InterfaceMethod java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
61: invokeinterface #8,  2            // InterfaceMethod java/util/Map.get:(Ljava/lang/Object;)Ljava/lang/Object;
66: checkcast     #9                  // class java/lang/String
69: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

72: return
复制代码

能够看到两部分代码在编译后是同样的。 第0行,new HashMap<String, String>() 实际构造的是java/util/HashMap。 第30行,stringMap.get("hello") ,checkcast指令,作了一个类型转换

2. 历史背景

2004年,Java5.0。为了保证代码的“二进制向后兼容”,引入泛型后,原先的代码必须可以编译和运行。例如Java数组支持协变,集合类也能够存入不一样类型元素。 代码以下

Object[] array = new String[10]; 
array[0] = 10; // 编译期不会有问题,运行时会报错 

ArrayList things = new ArrayList(); 
things.add(Integer.valueOf(10)); //编译、运行时都不会报错
things.add("hello world");
复制代码

若是要保证Java5.0引入泛型后,上述代码依然能够运行,有两个选择:

  • 原先须要泛型化的类型保持不变,再新增一套泛型化的类型版本。泛型具现化,好比C#新增了一组System.Collections.Generic的新容器,原先的System.Collections保持不变。
  • 把须要泛型化的类型原地泛型化,Java5.0采用的原地泛型化方式为类型擦除。

为什么C#与Java的选择不一样,主要是C#当时才2年遗留老代码少,Java快10年了老代码多。类型擦除是偷懒留下的技术债。

3.类型擦除

类型擦除除了前面提到的编译后都变成了统一的裸类型以及使用时的类型检查和转换以外还有其余缺陷。 1)不支持原始类型(Primitive Type)数据泛型,ArrayList须要使用其对应引用类型ArrayList,致使了读写的装箱拆箱。 2)运行期没法获取泛型类型信息,例如

public  <E> void doSomething(E item){
        E[] array=new E[10];  //不合法,没法使用泛型建立数组
        if(item instanceof  E){}//不合法,没法对泛型进行实例判断
}
复制代码

当咱们去写一个List到数组的转换方法时,须要额外传递一个数组的组件类型

public  static <T> T[] convert(List<T> list,Class<T> componentType){
        T[] array= (T[]) Array.newInstance(componentType,list.size());
        for (int i = 0; i < list.size(); i++) {
            array[i]=list.get(i);
        }
        return array;
}
复制代码

3)类型转换问题。

//没法编译经过
//虽然String是Object的子类,但ArrayList<String>并非ArrayList<Object>的子类。
ArrayList<Object> list=new ArrayList<String>();
复制代码

为了支持协变和逆变,泛型引入了 extends ,super

//协变 
ArrayList<? extends Object> list = new ArrayList<String>();

//逆变
ArrayList<? super String> list2 = new ArrayList<Object>();
复制代码

4 值类型与将来泛型

2014年,Oracle,Valhalla语言改进项目内容之一,新泛型实现方案

10.3.2 其余

自动装箱,自动拆箱,遍历循环,变长参数,条件编译,内部类,枚举类,数值字面量,switch,try等等。

10.3.3 *扩展阅读

Java协变介绍 Lambda与invokedynamic

10.4 实战 Lombok注解处理器