第四部分 程序设计与代码优化
第10章 早期(编译器)优化
计算机程序对效率地追求就是程序天生的坚定信仰
10.1 概述
- 前端编译器:把*.java文件变成.class文件的过程,Sun的javac、Eclipse JDT中的增量式编译器(ECJ)
- JIT编译器,Just In Time Compiler,后端运行期编译器:把字节码转变成机器码的过程,HotSpot VM的C1、C2编译器
- AOT编译器,Ahead Of Time Compiler,静态提前编译器:GNU Compiler for the Java(GCJ)、Excelsior JET
- Java中即时编译器在运行期的优化过程对于程序运行来说更重要,而前端编译器在编译器的优化过程对于程序编码来说关系更加密切
10.2 Javac编译器
Javac编译器是一个由Java语言编写的程序
10.2.1 Javac的源码与调试
- Javac的源码存放在JDK_SRC_HOME/langtools/src/share/classes/com/sun/tools/javac中,除了JDK自身的API外,就只引用了JDK_SRC_HOME/langtools/src/share/classes/com/sun/*里面的代码
- Sun Javac的编译过程大致可以分成3个过程,分别是:
- 解析与填充符号表过程
- 插入式注解处理器的注解处理过程
- 分析与字节码生成过程
10.2.2 解析与填充符号表
- 解析步骤由parseFiles()方法完成
- 解析步骤包括经典程序编译原理中的词法分析和语法分析两个过程
- 词法、语法分析
- 词法分析是将源代码的字符流转变为标记(Token)集合
- 单个字符是程序编写过程中的最小元素
- 标记是编译过程的最小元素,关键字、变量名、字面量、运算符都可以成为标记
- 语法分析是根据Token序列构造抽象语法树的过程
- 抽象语法树(Abstract Syntax Tree,AST)是一种用来描述程序代码语法结构的树形表示方式
- 语法树的每一个节点都代表着程序代码中的一个语法结构(Construct),例如包、类型、修饰符、运算符、接口、返回值、代码中的注释
- Javac源码中,语法分析过程由com.sun.tools.javac.parser.Parser类实现,这个阶段产出的抽象语法树由com.sun.tools.javac.tree.JCTree
- 填充符号表
- 符号表(Symbol Table)是由一组符号地址和符号信息构成的表格
- 在语义分析中,符号表所登记的内容将用于语义检查和产生中间代码
- 在目标代码生成阶段,当对符号表进行地址分配时,符号表是地址分配的依据
- Javac源代码中,填充符号表的过程由com.sun.tools.javac.comp.Enter类实现
10.2.3 注解处理器
- 在Javac源码中,插入式注解处理器的初始化过程是在initPorcessAnnotation()方法中完成的,而它的执行过程则是在processAnnotation()方法中完成的
10.2.4 语义分析与字节码生成
- 语义分析的主要任务是对结构上正确的源程序进行上下文有关性质的审查
- 标注检查
- 语义分析过程分为标注检查以及数据及控制流分析两个步骤,分别有attribute()和flow()方法完成
- 标注检查步骤检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配以及常量折叠等
- 标注检查步骤在Javac源码中的实现类是com.sun.tools.javac.comp.Attr类和com.sun.tools.javac.comp.Check类
- 数据及控制流分析
- 数据及控制流分析是对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都被正确处理了等问题。
- 数据及控制流分析的入口是flow方法,具体操作由com.sun.tools.javac.comp.Flow类来完成
- 解语法糖
- 语法糖(Syntactic Sugar),也称糖衣语法,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用
- Java在现代编程语言中属于“低糖语言”
- 在Javac源码中,解语法糖的过程由desugar()方法触发,在com.sun.tools.javac.comp.TransTypes类和com.sun.tools.javac.comp.Lower类中完成
- 字节码生成
- 字节码生成是Javac编译过程的最后一个阶段,在Javac源码里面由com.sun.tools.javac.jvm.Gen类来完成
10.3 Java语法糖的味道
语法糖虽然不会提供实质性的功能改进,但是它们或能提高效率,或能提升语法的严谨性,或能减少编码出错的机会
10.3.1 泛型与类型擦除
- 泛型的本质是参数化类型(Parametersized Type)的应用,也就是说所操作的数据类型被指定为一个参数
- 这种类型可以在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法
- Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型
- Signature的作用是存储一个方法在字节码层面的特征签名,这个属性中保存的参数类型不是原生类型,而是包括了参数化类型的信息
10.3.2 自动装箱、拆箱与遍历循环
10.3.3 条件编译
- C、C++中使用预处理器指示符(ifdef)来完成条件编译
- Java语言使用条件为常量的if语句
10.4 实战:插入式注解处理器
- Java语言中,运行期即时编译与虚拟机执行子系统非常紧密的互相依赖、配合运作
10.4.1 实战目标
- 使用注解处理器API编写一款拥有自己编码风格的检验工具NameCheckProcessor
- NameCheckProcessor的目标仅定为对Java程序命名进行检查
- 类(或接口):符合驼式命名法,首字母大写
- 方法:符合驼式命名法,首字母小写
- 字段:
- 类或实例变量:符合驼式命名法,首字母小写
- 常量:要求全部由大写字母或下划线构成,并且第一个字符不能是下划线
10.4.2 代码实现
- 实现注解处理器的代码需要继承抽象类javax.annotation.processing.AbstractProcessor
- 抽象类中只有一个必须覆盖的abstract方法:“process()”,它是Javac编译器在执行注解处理器代码时要调用的过程
- 这个方法的第一个参数“annotation”中获取到此注解处理器所要处理的注解集合,第二个参数“roundEnv”中访问到当前这个Round中的语法树节点,每个语法树节点在这里表示为一个Element。
- 16类Element:
- 包,PACKAGE
- 枚举,ENUM
- 类,CLASS
- 注解,ANNOTATION_TYPE
- 接口,INTERFACE
- 枚举值,ENUM_CONSTANT
- 字段,FILELD
- 参数,PARAMETER
- 本地变量,LOCAL_VARIABLE
- 异常,EXCEPTION_PARAMETER
- 方法,METHOD
- 构造函数,CONSTRUCTOR
- 静态语句块,STATIC_INIT
- 实例语句块,INSTANCE_INIT
- 参数化类型,TYPE_PARAMETER
- 未定义的其他语法树节点,OTHER
- "processEnv"是AbstractProcessor中的一个protected变量,代表了注解处理器框架提供的一个上下文环境,要创建新的代码、向编译器输出信息、获取其他工具类等都需要用到这个实例
- Annotation:@SupportedAnnotationTypes代表了这个注解处理器对哪些注解感兴趣,可以使用星号“*”作为通配符代表对所有的注解都感兴趣
- @SupportSourceVersion指出这个注解处理器可以处理哪些版本的Java代码
- 如果不需要改变或生成语法树的内容,process()方法就可以返回一个值为false的布尔值
- NameChecker通过继承于javax.lang.model.util.ElementScanner的NameCheckScanner类,以Visitor模式来完成对语法树的遍历,以visitor模式来完成对语法树的遍历,分别执行visitType()、visitVariable()和visitExecutable()方法来访问类、字段和方法
- checkCamelCase()与checkAllCaps()方法则用于实现驼式命名法和全大写命名规则的检查
10.4.3 运行与测试
- 通过Javac命令的“-processor”参数来执行编译时需要附带的注解处理器,如果有多个注解处理器的话,用逗号分隔。
- 还可以使用-XprintRounds和-XprintProcessorInfo参数来查看注解处理器运作的详细信息
10.4.4 其他应用案例
- 基于NameCheckProcessor这组API支持的项目还有用于校验的Hibernate标签使用正确性的Hibernate Validator Annotation Processor、自动为字段生成getter和setter方法的Project Lombok(根据已有元素生成新的语法树元素)等
10.5 本章小结
在前端编译器中,“优化”手段主要用于提升程序的编码效率
第11章 晚期(运行期)优化
11.1 概述
- Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”(Hot Spot Code)
- 为提高热点代码的执行效率,虚拟机会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler)
- 即时编译器性能的好坏、代码优化程度的高低却是衡量一款商用虚拟机优秀与否的最关键的指标之一,也是虚拟机中最核心且最能体现虚拟机技术水平的部分
11.2 HotSpot虚拟机内的即时编译器
- 解决以下问题:
- 为何HotSpot虚拟机要使用解释器与编译器并存的架构
- 为何HotSpot虚拟机要实现两个不同的即时编译器
- 程序何时使用解释器执行?何时使用编译器执行?
- 哪些程序代码会被编译为本地代码?如何编译为本地代码?
- 如何从外部观察即时编译器的编码过程和编码结果?
11.2.1 解释器与编译器
- 解释器与编译器两者各有优势:
- 当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译时间,立即执行
- 在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获得更高的执行效率
- 当程序运行环境中内存资源限制较大(如部分嵌入式系统中),可以使用解释执行节约内存,反之可以使用编译执行来提升效率
- 解释器可以作为编译器激进优化时的一个“逃生门”,让编译器根据概率选择一些大多数时候都能提升运行速度的优化手段
- HotSpot虚拟机中内置了两个即时编译器,分别称为Client Compiler和Server Compiler,或者简称为C1编译器和C2编译器(也叫做Opto编译器)
- 解释器与编译器搭配使用地方式在虚拟机中称为”混合模式“(Mixed Mode)
- 用户可以使用参数”-Xint“强制虚拟机运行于”解释模式“(Interpreted Mode),这时编译器完全不介入工作,全部代码都使用解释方式执行
- 使用参数“-Xcomp”强制虚拟机运行于"编译模式"(Compiler Mode),这时将优先采用编译方式执行程序,但是解释器仍然要在编译无法进行的情况下介入执行过程
- 为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机会逐渐启用分层编译(Tiered Compilation)的策略
- 分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次,其中包括:
- 第0层,程序解释执行,解释器不开启性能监控功能(Profiling),可触发第1层编译
- 第1层,也称为C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要将加入性能监控的逻辑
- 第2层(或2层以上),也称为C2编译,也就是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化
11.2.2 编译对象与触发条件
- “热点代码”有两类:
- 被多次调用的方法
- 方法体内代码执行次数多
- 编译器会以整个方法作为编译对象,标准的JIT编译方式
- 被多次执行的循环体
- 一个方法只被调用过一次或少量的几次,但是方法体内部存在循环次数较多的循环体的问题
- 编译器依然会以整个方法(而不是单独的循环体)作为编译对象,栈上替换编译方式(On Stack Replacement,简称为OSR编译,即方法栈帧还在栈上,方法就被替换了)
- 被多次调用的方法
- 热点探测,Hot Spot Detection:判断一段代码是不是热点代码,是不是需要触发及时编译
- 基于采样的热点探测(Sample Based Hot Spot Detection):采用这种方法的虚拟机会周期性地检查各个线程的栈顶,如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是“热点方法”
- 好处是实现简单、高效
- 很难精准地确认一个方法的热度,容易受到线程阻塞或别的外界因素的影响而扰乱热点探测
- 基于计数器的热点探测(Counter Based Hot Spot Detection):采用这种方法的虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数如果执行次数超过一定的阈值就认为它是“热点方法”
- 统计结果相对来说更加精确和严谨
- 实现起来麻烦一些
- 基于采样的热点探测(Sample Based Hot Spot Detection):采用这种方法的虚拟机会周期性地检查各个线程的栈顶,如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是“热点方法”
- HotSpot虚拟机使用的是第二种,为每个方法准备了两类计数器:
- 方法调用计数器,Invocation Counter
- 用于统计方法被调用的次数
- 默认阈值在Client模式下是1500
- 次,在Server模式下是10000次,这个阈值可以通过虚拟机参数-XX:CompileThreshold来人为设定
- 如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数
- 当超过一定的时间限度,如果方法的调用次数不足以让它提交给即时编译器编译,那这个方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度的衰减(Counter Decay)
- 这段时间被称为此方法统计的半衰周期(Counter Half Life Time)
- 回边计数器,Back Edge Counter
- 方法调用计数器,Invocation Counter
11.3 编译优化技术
- 以编译方式执行本地代码比解释方式更快
- 除去虚拟机解释执行字节码时额外消耗时间的原因外,还有一个很重要的原因是虚拟机设计团队几乎把对代码的所有优化措施都集中在了即时编译器中
11.3.1 优化技术概览
- 语言无关的经典优化技术之一:公共子表达式消除
- 语言相关的经典优化技术之一:数组范围检查消除
- 最重要的优化技术之一:方法内联
- 最前沿的优化技术之一:逃逸分析
11.3.2 公共子表达式消除
- 含义是:如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成为了公共子表达式
11.3.3 数组边界检查消除
- 数组边界检查消除(Array Bounds Checking Elimination)是即时编译器中的一项语言相关的经典优化技术
11.3.4 方法内联
- 方法内联是编译器最重要的优化手段之一
- 方法内联是把目标方法的代码“复制”到发起调用的方法之中,避免发生真实的方法调用
- 按照经典编译原理的优化理论,大多数的Java方法都无法内联,无法内联的原因是Java语言中默认的实例方法是虚方法
- 为了解决虚方法的问题,Java虚拟机引入一种名为“类型继承关系分析”(Class Hierarchy Analysis,CHA)的技术,一种基于整个应用程序的类型分析技术
- CHA用于确定在目前已加载的类中,某个接口是否有多于一种的实现,某个类是否存在子类、子类是否为抽象类等信息
11.3.5 逃逸分析
- 逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术
- 逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用
- 方法逃逸:作为调用参数传递到其他方法中
- 线程逃逸:赋值给类变量或可以在其他线程中访问的实例变量
- 栈上分配(Stack Allocation):如果确定一个对象不会逃逸出方法之外,让这个对象在栈上分配内存,对象所占用的内存空间就可以随栈帧出栈而销毁
- 同步消除(Synchonization Elimination):对这个变量实施的同步措施可以消除
- 标量替换(Scalar Replacement):
- 标量(Scalar)是指一个数据已经无法在分解成更小的数据来表示了,Java虚拟机中的原始数据类型可以称为标量
- 聚合量(Aggregate):一个数据可以继续分解,例如Java中的对象
- 标量替换:把一个Java对象拆散,根据程序访问情况,将其使用到成员变量恢复原始类型来访问
- 逃逸分析不成熟的原因主要是不能保证逃逸分析的性能收益必定高于它的消耗
- -XX:+DoEscapeAnalysis参数手动开启逃逸
- -XX:+PrintEscapeAnalysis参数查看分析结果
- -XX:+EliminateAllocations参数开启标量替换
- +XX:+EliminateLocks参数来开启同步消除
- -XX:+PrintEliminateAllocations查看标量的替换情况
11.4 Java与C/C++的编译对比
- Java虚拟机的即使编译器与C/C++的静态优化编译器相比,可能会由于下列这些原因而导致输出的本地代码有一些劣势:
- 第一,即时编译器运行占用的是用户程序的运行时间,具有很大的时间压力,编译的时间成本并不是静态优化编译器主要的关注点
- 第二,Java语言是动态地类型安全语言,从实现层面上看,这意味着虚拟机必须频繁地进行动态检查
- 第三,Java语言中虽然没有virtual关键字,但是虚方法的频率远远大于C
- 第四,Java语言是可以动态扩展的语言,这使得很多全局的优化都难以进行
- 第五,Java语言中对象的内存分配都是堆上进行的,只有方法中的局部变量才能在栈上分配,而C/C++的对象则有多种内存分配方式
- 动态安全、动态拓展、垃圾回收都为Java语言的开发效率做出来很大贡献
- 在C/C++中,别名分析(Alias Analysis)的难度要远高于Java
11.5 本章小结