前端编译(Javac编译器)
Javac的编译过程(将源代码转换为字节码)
- 解析源代码,填充符号表
源代码字符流通过词法分析和语法分析,生成出抽象的语法树,通过语法树填充符号表,因为语法树和符号表包含了源代码的所有有关的信息,所有该阶段后源代码字符流被完全抛弃。符号表可以理解为一个记录了所有源代码相关信息的数据结构
注:词法分析是将源代码字符流转换为Token集合的过程,例如int a = 3 + 2中,Token就是int、b、=、3、+、2。语法分析是根据Token集合生成语法树的过程。
-
插入式注解处理器
插入式注解处理器可以实现编译期间更新语法树,从而修改符号表。达到动态修改源代码的作用,如Lombok工具。 -
语义分析
生成抽象的语法树后,保证了程序结构正确,但是无法保证代码符合逻辑,因此要进行语义分析。语义分析包括标注检查和数据与控制流分析。
注:标注检查是一个局部的检查,例如变量是否声明,类型是否一致。数据与控制流分析是一种全局的检查,主要是检查程序的上下文逻辑是否出现问题。 -
解语法糖,生成字节码
生成字节码就是将前几个阶段得到的符号逻辑的信息转换为字节码的同时,进行了少量代码添加和转换的工作。
总的流程:解析源代码,填充符号表–>插入注解处理器,更新符号表–>语义分析,保证程序符号逻辑–>解语法糖,生成字节码
Java泛型
Java的泛型采用的是类型擦除的方式。类型擦除(Type Erasure),泛型在源码中存在,但是字节码中不存在泛型的信息。
List<Integer> intList = new ArrayList<>();
List<String> strList = new ArrayList<>();
// 上面代码经过编译后,然后再反编译回去,会变成下面的形式
List intList = new ArrayList();
List strList = new ArrayList();
// 这种List类型称为裸类型(Raw Type)
类型擦除的实现:
- 裸类型,所有该类型泛型化实例的共同父类。
- 简单的粗暴的在编译时,将泛型化实例转换为裸类型。
出现的问题:
- 因为int、long等基本类型无法与Object强转,所有Java泛型不支持int、long等基本类型
- 大量的装箱、拆箱、类型转换,让效率较低
- 运行期间无法得到泛型信息,让编程变得复杂。
后端编译
HotSpot、OpenJ9等主流JVM,都是采用解释器和编译器并存的架构,解释器让程序的启动更加快,编译器让程序的执行速度更快,同时这些主流的JVM会在执行的过程中进行热点探测,找出热点代码,进行编译,让程序执行速度越来越快。
热点探测(如何找出热点代码)
首先热点代码主要有2类,常调用的方法和多次执行的循环体,编译的目标都是以方法为单位,只是多次的循环体的入口可能不会在方法的开头。
代码探测主要有2种
- 基于采样的热点探测,JVM会周期性对栈顶的方法进行检查,得到热点代码块,这样的好处是简单高效,并且可以容易获得方法之间的调用关系,缺点就是不够精确。
- 基于计数器的热点探测,包含两种计数器,方法调用计数器和回边计数器,分别用来统计方法调用的次数和循环体的执行次数。每一次调用方法都会让方法计数器值加1,当计数器的值超过某个阈值就会向编译器提交编译请求,方法调用计数器存在一个半衰期,经过一个半衰期后,计数器的值就会减少一半。同理回边计数器也每次执行方法体的时候,会让回边计数器加1,超过阈值就会触发栈上的替换编译。
注:编译器通常有C1(客户端编译器),C2(服务端编译器),c1注重编译的速度,c2注重编译的质量。