- 所谓编译
- 前端编译器:将java文件转变为class文件,通常意义上的Java程序的编译
- 后端运行期编译器(JIT,Just In Time),将字节码class文件转变为机器码的过程
- 静态提前编译器(AOT,Ahead Of Time),将java文件转变为本地机器码的过程
- 所谓编译期优化
- 前端编译器:
- 对代码的运行效率几乎没有优化措施(JDK1.3之后,javac的-O优化参数不再有意义)
- 语法糖,针对编码过程的优化措施,改善程序员的编码风格,提高编码效率
- 后端编译器:
- 集合了大量对运行性能的优化,使得优化效果不仅影响到java程序,还能惠及其他运行在虚拟机上的其他语言的class文件
- 前端编译器:
Javac编译器
- 编译过程:
- 解析与填充符号表(parseFiles方法)
- 词法分析:将源代码的字符流转变为token(关键字、变量名、字面量、运算符等)集合
- 语法分析:根据token序列构造抽象语法树(AST,Abstract Syntax Tree),每个节点代表程序代码中的一个语法结构(包、类型、修饰符、运算符、接口、返回值、注释等)
- 填充符号表:一组符号地址和符号信息构成的表格,此过程的出口是一个TODO list,包含每个编译单元的抽象语法树的顶级节点
- Java语言的天然编译方式:将所有的编译单元的语法树顶级节点输入到TODO list后再进行编译,各个文件直接能够互相提供符号信息,而不是一个个地编译Java文件
- 插入式注解处理器的注解处理过程
- 注解(Annotation)与普通Java代码一样,在运行期间发挥作用
- 注解处理器,在编译期间对注解进行处理,可视为编译器的插件,可以读取、修改、添加抽象语法树中的任意元素,使得代码可以干涉编译器行为
- 如果注解处理器修改了抽象语法树,则编译过程会回到起始的解析与填充符号表阶段,知道不再修改AST为止
- 分析与字节码生成
- 语义分析:上下文有关性质的审查,如类型审查
- 标注检查
- 变量是否被声明、变量与赋值是否类型匹配
- 常量折叠(a=2+3 -> a=5)
- 数据及控制流分析
- 与类加载时的数据及控制流分析目的类似,校验范围有所区别(如,将局部变量声明为final,对运行期没有影响,仅由编译器保证不变性)
- 变量是否有赋值,方法是否有返回值,异常处理
- 解语法糖
- 泛型与类型擦除
- C#的泛型是真实泛型,实现称为类型膨胀,List List在系统运行时是两个不同的类型,有自己的虚方法表和类型数据
- Java的泛型是伪泛型,实现称为类型擦除,只在程序源码中存在,编译后被替换为原生类型,并在相应地方插入强制类型转换代码
- 自动装箱、拆箱与遍历循环
- 自动装箱、拆箱在编译后被转化为对应的包装和还原方法
- 遍历循环咋把代码还原成迭代器实现
- 变长参数在调用时变成了数组类型的参数
- 条件编译
- if(true){…}else{…},else代码块不会被编译
- 只有使用条件为常量的if语句才能达到这种效果
- 只能实现语句基本块级别的条件编译
- 泛型与类型擦除
- 字节码生成
- 将前面各个步骤生成的信息(AST&TODO list)转化为字节码写入磁盘
- 代码添加和转换工作
- init方法:实例构造器,将语句块、实例变量初始化、调用父类的实例构造器等操作收敛到init方法中
- clinit方法:类构造器,将static语句块、类变量初始化、Object类的init方法等操作收敛到clinit方法中
- 代码替换以便优化程序:字符串的加操作替换为StringBuilder的append操作
- 标注检查
- 语义分析:上下文有关性质的审查,如类型审查
- 解析与填充符号表(parseFiles方法)
- 编译过程:
即时编译器:为提高热点代码的执行效率,将这些代码编译成与本地平台相关的机器码,并进行各层次优化
- 解释器 VS 编译器
- 主流虚拟机都采用解释器与编译器并存的架构
- 解释器省去编译时间,立即执行,迅速启动
- 编译器富含优化,提升效率和运行速度,激进优化失败后退回解释器执行(逃生门)
- HotSpot虚拟机内置两个JIT
- client compiler(C1编译器)
- 关注局部性的优化
- 三段式编译器
- 字节码->基础优化,方法内联、常量传播等->高级中间代码(HIR,High-Level Intermediate Representation)-> 进阶优化,空值检查消除、范围检查消除等 -> 低级中间代码(LIR)-> 现行扫描算法、窥孔优化(??)-> 机器代码
- server compiler(C2编译器)
- 全局性优化,几乎达到C++编译器-O2的优化强度
- 根据profiling进行不可靠的激进优化
- client compiler(C1编译器)
- 分层编译
- 第0层,解释执行,不开启性能监控(profiling)
- 第1层,C1编译,简单可靠优化,可加入profiling
- 第2层,C2编译,启用编译耗时较长的优化,根据profiling进行不可靠的激进优化
- 热点代码
- 被多次调用的方法
- 以整个方法作为编译对象
- 被多次执行的循环体
- 以整个方法(而不是循环体)为编译对象
- 编译发生在方法执行过程中,栈上替换(OSR, On Stack Replacement),方法栈帧还在栈上就被替换了
- 热点探测判定方式
- 基于采样的热点探测
- 周期性检查各线程的栈顶
- 简单高效,容易获取方法调用关系(展开调用堆栈即可)
- 精确度不高,易受线程阻塞和其他外界因素影响
- 基于计数器的热点探测(HotSpot采用)
- 计数器超过一定阈值判定
- 麻烦,需维护计数器,不能直接获取调用关系
- 精确严谨
- 基于采样的热点探测
- HotSpot采用的基于计数器的热点探测
- 方法调用计数器
- 多次调用的方法
- 非绝对调用次数,而是一段时间内的调用次数
- 存在热度衰减,超过一段时间后以统计的次数变少
- 回边计数器
- 循环体
- 绝对次数,无衰减
- 方法调用计数器
- 被多次调用的方法
- 编译优化技术
- 方法内联:去除调用成本,便于在更大范围上采取后续优化手段
- 按照经典编译原理的优化理论,大多数Java方法不能内联,因为java方法调用需要在运行时进行方法接受者的多态选择
- 解决虚方法与内联的矛盾:类型继承关系分析(CHA, Class Hierarchy Analysis)
- 如果是非虚方法,则内联
- 虚方法,向CHA查询当前程序下是否有多个目标版本
- 一个版本,则内联
- 多个版本,则内联缓存
- 未发生方法调用前,缓存状态为空
- 第一次调用后,记录下方法接受者的版本信息
- 下次调用读取缓存
- 如果之后的每次调用,方法接受者版本一样,则一直内联
- 否则,取消内联,查找虚方法表进行方法分派
- 冗余访问消除
- 复写传播
- 无用代码消除
- 公共子表达式消除
- 数组边界检查消除
- 逃逸分析:分析一个对象是否会逃逸到方法或线程之外,别的方法或线程是否会访问该对象
- 基本行为:分析对象动态作用域
- 不会逃逸的对象,可进一步优化
- 栈上分配
- 同步消除
- 标量替换
- 方法内联:去除调用成本,便于在更大范围上采取后续优化手段
- 解释器 VS 编译器
- Java编译器 VS C++编译器
- JIT占用用户程序的运行时间
- java语言是动态的类型安全语言,频繁进行动态检查
- java语言虽然没有virtual关键字,但使用虚方法的频率远大于C++,动态选择频率也更大,优化难度更大
- java语言动态可扩展,加载新的类可能改变程序类型的继承关系,很难看见程序全貌,全局优化难以进行
- java语言的对象内存分配在堆上,C++允许多种内存分配方式,Java的内存回收压力更大
- java语言的开发效率更高
- java编译器可进行以运行期性能监控为基础的优化措施,C++只能进行编译期间的优化
深入理解JAVA虚拟机——总结5 Java编译器及优化
最新推荐文章于 2021-11-19 15:57:27 发布