深入理解JAVA虚拟机——总结5 Java编译器及优化

来源:互联网 发布:上海游族网络招聘电话 编辑:程序博客网 时间:2024/06/07 18:54
  • 所谓编译
    • 前端编译器:将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操作
  • 即时编译器:为提高热点代码的执行效率,将这些代码编译成与本地平台相关的机器码,并进行各层次优化

    • 解释器 VS 编译器
      • 主流虚拟机都采用解释器与编译器并存的架构
      • 解释器省去编译时间,立即执行,迅速启动
      • 编译器富含优化,提升效率和运行速度,激进优化失败后退回解释器执行(逃生门)
      • HotSpot虚拟机内置两个JIT
        • client compiler(C1编译器)
          • 关注局部性的优化
          • 三段式编译器
            • 字节码->基础优化,方法内联、常量传播等->高级中间代码(HIR,High-Level Intermediate Representation)-> 进阶优化,空值检查消除、范围检查消除等 -> 低级中间代码(LIR)-> 现行扫描算法、窥孔优化(??)-> 机器代码
        • server compiler(C2编译器)
          • 全局性优化,几乎达到C++编译器-O2的优化强度
          • 根据profiling进行不可靠的激进优化
      • 分层编译
        • 第0层,解释执行,不开启性能监控(profiling)
        • 第1层,C1编译,简单可靠优化,可加入profiling
        • 第2层,C2编译,启用编译耗时较长的优化,根据profiling进行不可靠的激进优化
    • 热点代码
      • 被多次调用的方法
        • 以整个方法作为编译对象
      • 被多次执行的循环体
        • 以整个方法(而不是循环体)为编译对象
        • 编译发生在方法执行过程中,栈上替换(OSR, On Stack Replacement),方法栈帧还在栈上就被替换了
      • 热点探测判定方式
        • 基于采样的热点探测
          • 周期性检查各线程的栈顶
          • 简单高效,容易获取方法调用关系(展开调用堆栈即可)
          • 精确度不高,易受线程阻塞和其他外界因素影响
        • 基于计数器的热点探测(HotSpot采用)
          • 计数器超过一定阈值判定
          • 麻烦,需维护计数器,不能直接获取调用关系
          • 精确严谨
      • HotSpot采用的基于计数器的热点探测
        • 方法调用计数器
          • 多次调用的方法
          • 非绝对调用次数,而是一段时间内的调用次数
          • 存在热度衰减,超过一段时间后以统计的次数变少
        • 回边计数器
          • 循环体
          • 绝对次数,无衰减
    • 编译优化技术
      • 方法内联:去除调用成本,便于在更大范围上采取后续优化手段
        • 按照经典编译原理的优化理论,大多数Java方法不能内联,因为java方法调用需要在运行时进行方法接受者的多态选择
        • 解决虚方法与内联的矛盾:类型继承关系分析(CHA, Class Hierarchy Analysis)
          • 如果是非虚方法,则内联
          • 虚方法,向CHA查询当前程序下是否有多个目标版本
            • 一个版本,则内联
            • 多个版本,则内联缓存
              • 未发生方法调用前,缓存状态为空
              • 第一次调用后,记录下方法接受者的版本信息
              • 下次调用读取缓存
                • 如果之后的每次调用,方法接受者版本一样,则一直内联
                • 否则,取消内联,查找虚方法表进行方法分派
      • 冗余访问消除
      • 复写传播
      • 无用代码消除
      • 公共子表达式消除
      • 数组边界检查消除
      • 逃逸分析:分析一个对象是否会逃逸到方法或线程之外,别的方法或线程是否会访问该对象
        • 基本行为:分析对象动态作用域
        • 不会逃逸的对象,可进一步优化
          • 栈上分配
          • 同步消除
          • 标量替换
  • Java编译器 VS C++编译器
    • JIT占用用户程序的运行时间
    • java语言是动态的类型安全语言,频繁进行动态检查
    • java语言虽然没有virtual关键字,但使用虚方法的频率远大于C++,动态选择频率也更大,优化难度更大
    • java语言动态可扩展,加载新的类可能改变程序类型的继承关系,很难看见程序全貌,全局优化难以进行
    • java语言的对象内存分配在堆上,C++允许多种内存分配方式,Java的内存回收压力更大
    • java语言的开发效率更高
    • java编译器可进行以运行期性能监控为基础的优化措施,C++只能进行编译期间的优化
0 0