执行引擎(三):程序编译与代码优化

  • 早期(编译期)优化
  • java语言编译期
    • 1.前端编译器(编译器的前端):*.java文件 => .class文件
      • Sun的Javac,Eclipse_JDT中的增量式编译器(ECJ)
    • 2.后端运行期编译器(JIT编译器):字节码转 => 机器码
      • HotSpot_VM的C1、C2编译器
    • 3.静态提前编译器(AOT编译器):.java文件 => 本地机器代码
      • CNU_Compilr_for_the_Java(GCJ)、Excelsior_JET
    • 注:虚拟机设计:性能优化集中到后端即时编译期中
      • 抽象优化;class文件受益(JRuby、Groovy等语言的Class文件等)。跨平台
  • Javac编译器
    • Javac编译过程
      • 1.解析与填充符号表过程
        • 词法、语法分析
          • 词法分析
            • 概念:源代码的字符流 => 标记集合
              • 单个字符:程序编写过程最小元素
              • 标记:即编译过程的最小元素(关键字,变量名,字面量,运算符等)
                • 由com.sun.tools.javac.parser.Scanner类实现
          • 语法分析
            • 概念:根据Token序列构造抽象语法树
              • 抽象语法树:树形标识描述程序代码语法结构
                • 语法树节点:代表程序语法结构。
                  • 如包、类型、修饰符、运算符、接口、返回值、注释等
                  • 注:编译器经过语法分析后,不会对源码文件进行操作,后续操作建立在抽象语法树上
                • 由com.sun.tools.javac.parser.Parser类实现
                • 由com.sub.tools.javac.tree.JCTree类表示
        • 填充符号表
          • 符号表:符号地址:符号信息(K-V形式)表格
            • 编译不同阶段使用
            • 语义分析中,符号表信息用于语义检查(检查一个名字的使用和原先的说明是否一致) ,产生中间代码
            • 目标代码生成阶段:符号表:对符号名 进行地址分配时依据
            • 由com.sun.tools.javac.comp.Enter类实现:
              • 返回待处理表(包含编译单元的抽象语法树,package-info.java)的顶级节点)
      • 2.插入式注解处理器的注解处理过程
        • 注解处理器:编译期间对注解进行处理(读取,修改,添加 抽象语法树中的任意元素)
          • JDK1.5之后提供了对注解的支持,运行期间发挥作用。
          • JDK1.6之后提供了插入式注解处理器标准API,
        • 注:如插入式注解处理器在处理注解期间对语法树进行了修改,编译器将重新填充符号表过程
          • 直到所有的插入式注解处理器都没有再对语法树进行修改为止
          • 初始化过程在initProcessAnnotations()方法中完成
          • 执行过程在processAnnotation()方法中完成:
          • 判断是否还有新的注解处理器需要执行
          • 如果有,通过com.sun,tools.javac.processing.JacacProcessingEnvironment类的doProcessing()方法生成一个新JavaCompiler对象对编译的后续步骤进行处理
      • 3.分析与字节码生成过程
        • 语义分析与字节码生成
          • 语义分析的任务:
            • 对结构上进行上下文审查:
            • 类型审查等
          • 语义分析
            • 1.标注检查
              • 内容:检查变量使用前声明,变量赋值数据类型匹配
              • 常量折叠:如int a = 1 + 2, 经过常量折叠后会被折叠为字面量3,
                • 即代码里定义a=1+2比起a=3并不会增加程序运行期的运算量 
                • 由com.sun.tools.javac.comp.Attr
                • com.sun.tools.javc.comp.Check类实现
            • 2.数据及控制分析
              • 内容:检查局部变量使用前赋值,方法返回值受查异常正确处理等
                • 如:将布局变量声明为final,对运行期无影响,不变性编译期间保障,
                • 局部变量在常量池中没有CONSTANT_Fieldref_info的符号引用,即没有访问标志的信息,可能名称都保存(取决于编译时的选项)
              • 由com.sun.tools.javac.comp.Flow类来完成
            • 3.解语法糖
              • 语法糖:指在计算机语言中添加的某种语法,对语言的功能并无影响,但是更方便程序员使用
                • 如:泛型,编程参数,自动装箱/拆箱等
              • 注:虚拟机运行时不支持这些语法,编译阶段还原会最基础语法结构,即解语法糖
            • Java语法糖的味道
              • 泛型与类型擦除
                • JDK1.5的一项新增特性
                • 本质:参数化类型的应用。
                  • 即所操作的数据类型被指定为一个参数,
                  • 可用在类、接口、和方法创建中,称为泛型类,泛型接口和泛型方法
              • C#和Java中的泛型
                • C#泛型:类型膨胀
                  • C#泛型 程序源码、编译后的IL(中间语言,这时候的泛型是一个占位符)运行期的CLR中 确切存在
                  • List<String>和List<Integer>就是两种不同的类型,系统运行期生成,有各自的虚方法表和类型数据,
                • 基于类型膨胀实现的泛型称为真实泛型
                • Java泛型:
                  • Java泛型只存在程序源码中
                  • 编译后的字节码文件中 已替换为 原来的原生类型(裸类型),且相应的地方插入了强制转型
                  • 运行期的java语言 List<int>和List<String>就是同一个类
                  • 泛型擦除前
                  • 泛型擦除后:编译为Class文件,然后反编译后
                • JDK1.6中泛型的问题
                  • 自动装箱,拆箱与遍历循环
                  • 自动装箱、拆箱与遍历循环前
                  • 自动装箱、拆箱与遍历循环后:编译为Class文件,然后反编译后
                  • 注:遍历循环即还原了迭代器的实现。即实现Iterable接口原因
                  • 自动装箱的陷阱:
                    • JDK包装类 == 运算在不遇到 算数运算符 情况 不会 自动拆箱
                    • equals()方法不处理数据转型
              • 条件编译
                • java语言的条件编译本质:语法糖  根据布尔把分支中不成立的代码块消除掉。编译器接触语法糖阶段完成。
                • 智能实现语句基本块级别的条件编译,而没有办法实现根据条件调整整个java类的结构
                • 条件编译前
                • 条件编译后:编译为Class文件,然后反编译后
            • 4.字节码生成
              • 字节码生成:前面生成信息(语法树,符号表) => 字节码  写到磁盘
                • 编译器进行少量代码添加,转换工作
              • 生成构造器
                • 如:实例构造器init方法和类构造器clinit方法
                • 产生过程本质:代码收敛的过程
                • 编译器 把语句块、变量初始化、调用父类的实例构造器 等操作 收敛到 实例构造器和 类构造器方法之中
                • 且保证按 执行父类的实例构造器,初始化变量,执行语句块的顺序进行
                • 由Gen.normalizeDefs()方法来实现
                • 注:该实例构造器并不是指默认构造器,
                • 如用户代码中无构造函数,编译器将会添加无参,访问性与当前类一致的 默认构造函数(填充符号表阶段就已经完成)
              • 代码替换
                • 用于优化程序的实现逻辑:
                  • 如:把字符串的加操作替换为 StrngBuffer或 StringBuider(取决于目标代码的版本是否大于或等于JDK1.5)append()操作等
                • 语法树遍历调整完成后,会把填充了所有所需信息的符号表交给
                  • com.sin.toos.javac.jvm.ClassWtiter类的writeClass()方法输出字节码生成最终Class文件结束
                  • 由com.sun.tools.javac.jvm.Gen类来完成

晚期(运行期)优化

  • 概述:

    • Java程序最初 通过解释器 进行解释执行的。

    • 热点代码:某个方法或代码块的运行频繁

    • 即时编译器:运行时,热点代码编译成 本地平台相关的机器码。并进行优化

    • 衡量一款商用虚拟机优秀(虚拟机技术水平体现):

      • 注:即时编译期编译性能的好坏,代码优化程度的高低
    • HotSpot虚拟机内的即时编译器

      • 解释器与编译器

        • 优点:

          • 编译器

            • 程序运行后,代码 => 本地代码 随时间成正比

            • 分类

              • C1:Client Compiler

                • 强制指定虚拟机运行在Client模式:-client

              • C2:Server Compiler

                • 强制指定虚拟机运行在Server模式:-server

          • 解释器

            • 不需编译。立即执行

            • 逆优化:编译器激进优化容灾

              • 激进优化不成立(如加载了新类后类型继承结构出现变化,出现罕见陷阱) 可通过逆优化 回滚 解释状态继续执行

            • 程序运行环境内存资源限制较大,使用解释执行 节约内存

            • 程序运行环境内存资源限制较小,使用编译执行 提升效率

            • 工作方式

              • 混合模式:默认解释器与C1或C2编译器配合工作。

                • 取决虚拟机运行的模式

                • 根据自身版本 宿主机 硬件性能 自动选择运行模式

              • 解释模式:全部 使用 解释方式 执行

                • 强制指定虚拟机运行与解释模式:-Xint

              • 编译模式:优先采用 编译方式 执行

                • 解释器 在 编译无法进行 情况 介入执行过程

                • 强制指定虚拟机运行与编译模式:-Xcomp

          • 分层编译

            • 为什么分层编译?

              • 即时编译器 编译本地 代码 需占用程序运行时间

              • 编译 优化程序更高 代码  时间长

              • 解释器 可能替 编译器 收集性能监控信息

            • 原理:根据编译器编译。优化的规模与耗时

            • 包括

              • 0层:

              • 程序解释执行,解释器不开启性能监控,可触发第一层编译

              • 1层:(C1编译)

                • 将字节码编译为本地代码,进行简单,可靠的优化

                • 如果有必要加入性能监控的逻辑

              • 2层或2层以上(C2编译)

                • 将字节码编译为本地代码,

                • 启用编译耗时优化

                • 性能监控信息进行不可靠激进优化

            • 编译对象与触发条件

            • 运行时即时编译器编译的热点代码

            • 包括

          • 被多次调用的方法

          • 即一个方法被调用多次,

            • 方法体内的代码执行多次

            • JIT编译方式

            • 由方法调用触发的编译

            • 编译器会以整个方法作为编译对象

            • 被多次执行的循环体

        • 即一个方法只被调用过一次或者几次,

      • 但是方法体内部存在循环次数较多的循环体

      • 栈上替换(OSR)

        • 由循环体触发的编译,

      • 编译器会以整个方法(而不是单独的循环体)作为编译对象

      • 这种编译方式发生在方法执行过程之中

        • 热点探测

      • 判断一端代码是不是热点代码

      • 是不是需要触发即时编译

      • 这种行为即为热点探测

        • 包括

      • 基于采样的热点探测

      • 虚拟机会周期性的检查各个线程的栈顶

      • 如果发现某个(或某些)方法经常出现在栈顶

      • 即为热点方法

        • 优点

    • 实现简单,高效

    • 很容易获取方法调用关系(将调用堆栈展开即可)

    • 缺点

    • 很难精确地确认一个方法的热度

    • 容易受到线程阻塞或别的外界因素的影响

    • 从而扰乱热点探测

    • HotStop使用:

    • 基于计数器的热点探测

    • 虚拟机会为每个方法(甚至是代码块)建立计数器

    • 统计方法的执行次数,

    • 如果执行次数超过阈值

    • 即为热点方法

    • 优点

    • 统计结果相对来说更加精确和严谨

    • 缺点

    • 实现复杂,需要为每个方法建立并维护计数器

    • 不能 直接获取到方法的调用关系

    • 包括

    • 方法调用计数器

    • 默认阈值:

    • Client模式:1500次

    • Server模式:10000次

    • 通过-XX:CompileThreshold指定

    • 运行流程

    • 1.当一个方法被调用,首先检查该方法是否存在被JIT编译过的版本

    • 2.1存在:优先使用编译后的本地代码来执行

    • 2.2不存在,将该方法的调用计数器值+1

    • 2.2.1判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值

    • 2.2.2如超过,则向即时编译器提交该方法代码编译的请求

    • 注“如不做任何设置。执行引擎并不会等待编译请求完成

    • 而是继续进入解释器按照解释方式执行字节码

    • 当提交的请求被编译器编译完成

    • 该方法的调用入口地址会被系统自动改写成新的

    • 下一次调用该方法时就会使用已编译的版本

    • 方法调用计数器热度的衰减

    • 如不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数

    • 而是一个相对的执行频率(即一段时间内方法被调用的次数)

    • 当超过一定时间限制。如果方法的调用次数仍然不足以让他提交给即时编译器编译

    • 则该方法的调用计数器就会被减少一半䘀ꗗߐĀ㞠苉

    • 方法统计半衰周期

    • 即方法调用计数器热度衰减的时间

    • 关闭热度衰减

    • -XX:UseCounterDecay

    • 设置半衰周期时间(单位秒)

    • -XX:CounterHafLifeTime

    • 回边计数器

    • 作用

    • 统计一个方法中循环体代码执行的次数(回边次数)

    • 建立回边计数器统计的目的就是为了触发OSR编译

    • 设置回边计数器的阈值

    • 设置方法调用计数器阈值:-XX:CompileThreshord

    • 计算公式

    • 虚拟机运行在Client模式

    • 方法调用计数器阈值(CompileThreshold)×OSR比率(OnstackReplacePercentage)/100

    • OSR比率:默认966

    • 回边计数器阈值:默认13995

    • 虚拟机运行在Server模式

    • 方法调用计数器阈值)×(OSR比率-解释器监控比率)/100

    • OSR比率:默认140

    • 解释器监控比率:默认33

    • 回边计数器阈值:默认10700

    • 运行过程

    • 1.当虚拟机遇到一条回边指令时,首先查找将要执行的代码片段是否有已经编译好的版本

    • 2.1如果有,将会优先执行已编译好的代码

    • 2.2如果没有,则把回边计数器的值加1

    • 2.2.1判断放到调用计数器与回边计数器值之和是否超过回边计数器的阈值

    • 2.2.2如果超过,则提交一个OSR的编译请求,并且把回边计数器的值降低

    • 以便等待编译器输出编译结果

    • 注:回边计数器没有技术热度衰减,因此这个计数器统计的就是该方法循环执行的绝对次数

    • 编译过程

    • 在默认设置下,虚拟机在代码编译器还未完成之前,都仍然将按照解释方式继续执行

    • 而编译动作则在后台的编译线程中执行

    • 禁止后台编译后,执行线程向虚拟机提交编译请求后将会一直等待

    • 直到编译过程完成后在开始执行编译器输出的本地代码

    • 设置虚拟机禁止后台编译:-XX:BackgroundCompliation

    • Client Compiler

    • 简单快速的三段式编译器,

    • 主要关注局部性的优化,

    • 而放弃了许多耗时较长的全局优化手段

    • 第一阶段:前端(平台独立)将字节码构造成高级中间代码表示HIR)

    • 静态单分配代表代码值

    • 可以使得在HIR的构造过程中和后进行的优化动作更容易实现

    • 在此之前编译器会在字节码上完成一部分优化:方法内联,常量传播等

    • 第二阶段:后端(平台无关)从HIR中产生一种低级中间代码表示(LIR)

    • 在此之前会在HIR上完成一些优化,

    • 如:空值检查消除,范围检查消除,使HIR标识达到更高效

    • 第三阶段:后端(平台相关)使用线性扫描算法在LIR上分配寄存器,

    • 并做窥孔优化,产生机器代码

    • Server Compiler

    • 面向服务端的典型应用,为服务端的性能配置特别调整过的编译器,充分优化过的高级编译器

    • 执行所有的经典优化动作

    • 如:无用代码剔除,循环表达式外提,消除巩固子表达,常量传播,基本块重排序

    • 实施与java语言特性相关的优化技术

    • 如:范围检查消除,空值检查消除

    • 根据解释器或Client Compiler提供的性能监控信息,进行不稳定激进优化

    • 如:守护内敛,分支频率预测等

    • ServerCompiler的寄存器分配器是一个全局着色分配器,

    • 它可以充分利用某些处理器架构上的大寄存器集合

    • 编译优化技术

    • 优化技术概览

    • 优化前原始代码

    • 内联后代码

    • 冗余存储消除的代码

    • 复写传播的代码

    • 进行无用代码消除的代码

    • 公共子表达式消除:

    • 语言无关的经典优化技术之一

    • 公共子表达式消除是一个普遍应用与各种编译器的经典优化技术

    • 全局公共子表达式消除

    • 概念

    • 优化的范围涵盖了多个基本块

    • 局部公共子表达式消除

    • 概念

    • 优化仅限于程序的基本块内

    • 概念

    • 如果一个表达式E已经计算过了,并且从先前的计算到现在E中的所有变量的值都没有发生变化,那么E即为公共子表达式


    • javac编译后

    • 未做任何优化

    • 当这段代码进入到虚拟机即时编译器后,它将进行如下优化:

    • 编译器检测到c*b与b*c”是一样的表达式,而且在计算期间b与c的值是不变的

    • 编译器还可能(取决于哪种虚拟机的编译器以及具体的上下文而定)进行另外一种优化:

    • 代数化简,把表达式变为

    • 实现

    • 对于公共子表达式,没有必要花时间再对它进行计算,只需要直接用前面计算过的表达式结果替代E就可以了

    • 数组范围检查消除:

    • 语言相关的经典优化技术之一

    • 数组边界检查消除是即时编译器中的一项语言相关的经典优化技术

    • 原因

    • 由于java语言中访问数组元素时,系统将会自动进行上下界的范围检查,这必定会造成性能负担。

    • 为了安全,数组边界检查是必须做的,

    • 但数组边界检查是否必须一次不漏的执行则是可以“商量”的事情。

    • 实现

    • 如编译器通过数据流分析判定数组下标的取值永远在[0,数组.length)之内,

    • 就可以把数组的上下界检查消除

    • 从更高的角度看,大量安全检查使编写java程序更简单,但也造成了更多的隐式开销,

    • 对于这些隐式开销,除了尽可能把运行期检查提到编译期完成的思路之外,还可以使用隐式异常处理

    • 虚拟机会注册一个SegmentFault信号的异常处理器(uncommon_trap()),

    • 这样x不为空时,不会额外消耗一次对foo判空的开销。

    • 这个过程必须从用户态转到内核态中处理,结束后再回到用户态,速度远比一次判空检查慢。

    • foo极少为空的时候,隐式异常优化是值得的,但假如foo经常为空的话,这样的优化反而会让程序更慢,

    • 还好HotSpot虚拟机足够“聪明”,它会根据运行期收集到的Profile信息自动选择最优方案。

    • 方法内联:

    • 最重要的优化技术之一

    • 作用

    • 消除方法调用的成本

    • 为其他优化手段建立良好的基础

    • 虚方法内联问题

    • 虚方法可能存在多余一个版本的接受者(最多在去除被final修饰的方法)

    • 即java语言中默认的实例方法是虚方法

    • 解决方法

    • 类型继承关系分析(CHA)

    • 基于整个应用程序的类型分析技术,

    • 用于确定在目前已加载的类中,某个接口是否有多于一种的实现

    • 某个类是否存在子类,子类是否为抽象类等信息

    • 只有一个

    • 守护内联

    • 可以进行内联,不过这种内联属于激进优化,需要预留一个“逃生门”,称为守护内联(GuardedInlining)

    • 如果程序的后续执行过程中,虚拟机一直没有加载到会令这个方法的接收者的继承关系发生变化的类,

    • 那这个内联游湖的代码就可以一直使用下去。

    • 否则,就需要抛弃已经编译的代码,退回到解释状态执行,或者重新进行编译

    • 多个方法

    • 内联缓存

    • 工作原理

    • 1.在未发生方法调用之前,内联缓存状态为空

    • 2.当第一次调用发生后,缓存记录下方法接收者的版本信息

    • 3.并且每次进行方法调用时都比较接收者版本

    • 4.1如果以后进来的每次调用的方法接收者版本都是一样的,那这个内联还可以一直用下去。

    • 4.2如果发生了方法接收者不一致的情况,就说明程序真正使用了虚方法的多态特性,这时才会取消内联,查找虚方法表进行方法分派

    • 注:在许多情况下虚拟机进行的内联都是一种激进优化,激进优化的手段在高性能的商用虚拟机中很常见,

    • 除了内联之外,

    • 对于出现概率很小(通过经验数据或解释器收集到的性能监控信息确定概率大小)的隐式异常,

    • 使用率很大的分支等都可以被激进优化移除

    • 如果真的出现了小概率事件,这时才会从逃生门回到解释状态重新执行

    • 非虚方法

    • 直接进行内联,这时候的内联是有稳定前提保障的

    • 逃逸分析:

    • 最前沿的优化技术之一

    • 作用

    • 为其他优化手段提供依据的分析技术

    • 基本行为

    • 分析对象动态作用域

    • 方法逃逸

    • 当一个对象在方法中被定义后,他可能被外部方法引用(作为调用参数传递到其他方法中)

    • 线程逃逸

    • 当一个对象在方法中被定义后,他可能被外部线程访问到(赋值给类变量或可以在其他线程中访问到的实例变量)

    • 如果一个对象不会逃逸到方法或线程外

    • (别的方法或者线程无法通过任何途径访问到这个对象)

    • 则可能为这个变量进行一些高效的优化

    • 栈上分配(Stack Allocation)

    • 原因

    • Java堆中的对象对于各个线程都是共享和可见的,只要持有这个对象的引用,就可以访问堆中存储的对象数据。

    • 虚拟机的垃圾收集系统可以回收堆中不再使用的对象,但回收动作无论是筛选可回收对象,还是回收和整理内存都需要耗费时间。

    • 实现

    • 将对象在栈上分配内存,这样就可以使对象所占内存空间随栈帧出栈而销毁,减小垃圾收集系统的压力

    • 注:HotSpot虚拟机目前的实现方式导致栈上分配实现起来比较复杂,因此在HotSpot中暂时还没有做这项优化

    • 同步消除(Synchronization Elimination)

    • 对象无法被其他线程访问,这个变量的读写肯定不会有竞争,对这个变量实施的同步措施也就可以消除掉

    • 标量替换(Scalar Replacement)

    • 标量

    • 指一个数据已经无法再分解成更小的数据来表示


    • Java虚拟机中的原始数据类型(int、long等数值类型以及reference类型等)

    • 聚合量

    • 如果一个数据可以继续分解

    • 概念

    • 把一个Java对象拆散,根据程序访问的情况,将其使用到的成员变量恢复原始类型来访问

    • 实现

    • 如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,

    • 那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。

    • 优点

    • 可以让对象的成员变量在栈上(栈上存储的数据,有很大的概率会被虚拟机分配至物理机器的高速寄存器中存储)分配和读写

    • 可以为后续进一步的优化手段创建条件

    • 缺点

    • 主要是不能保证逃逸分析的性能收益必定高于它的消耗。

    • 如果要完全准确地判断一个对象是否会逃逸,需要进行数据流敏感的一系列复杂分析,从而确定程序各个分支执行时对此对象的影响。 这是一个相对高耗时

    • 的过程

    • 如果分析完后发现没有几个不逃逸的对象,那这些运行期耗用的时间就白白浪费了

    • JVM设置参数

    • 开启逃逸分析:-XX:+DoEscapeAnalysis

    • 查看分析结果:-XX:+PrintEscapeAnalysis

    • 开启标量替换:-XX:+EliminateAllocations

    • 查看标量的替换情况:-XX:+PrintEliminateAllocations

    • 开启同步消除:+XX:+EliminateLocks

    • java与C/C++编译器对比

    • java:即时编译期

    • 1.即时编译器运行时占用的是用户程序的运行时间,因此即时编译器不敢随便引入大规模的优化技术

    • 2.java语言是动态的类型安全语言,这就意味着虚拟机必须频繁地进行安全检查

    • 3.java语言中虚方法的使用频率远远大于C/C++语言,

    • 导致即时编译器在进行一些优化时的难度要远大于C/C++的静态优化编译器

    • 4.java语言时可以动态扩展的语言,运行时加载新的类可能改变程序类型的继承关系,

    • 导致许多全局的优化措施都只能以激进优化的方式来完成

    • 5.java虚拟机中对象的内存分配都是在堆上进行的

    • C++:静态编译器

    • 1.编译的时间成本在静态优化编译器中并不是主要的关注点

    • 2.C/C++的对象则有多种分配方式,而且C/C++中主要由用户程序代码来回收分配的内存,

    • 因此运行效率上比垃圾收集机制要高

    • java语言相对C/C++的劣势都是为了换取开发效率上的优势而付出的代价,

    • 而且还有许多优化是java的即时编译器能做而C/C++的静态优化编译器不能做或者不好做的,

    • 如别名分析、调用频率预测、分支频率预测、裁剪为被选择的分支等

猜你喜欢

转载自www.cnblogs.com/lllllht/p/9184343.html