要点提炼| 理解JVM之程序编译&代码优化
2019-01-02 本文已影响22人
厘米姑娘
本篇将介绍程序编译时期的代码优化手段,分成两个阶段:
- 概述
- 早期(编译期)优化
- 晚期(运行期)优化
1.概述
a.由于对Java语言的编译期理解不同,可以分出几个时期:
-
前端编译器
- 作用:把Java代码转变成字节码
- 代表:Sun的Javac、Eclipse JDT中的增量式编译器(ECJ)
- 该时期的优化主要用于提升程序的编码效率
-
后端运行期编译器/JIT编译器
- 作用:把字节码转变成本地机器码
- 代表:HotSpot VM的C1、C2编译器
- 该时期的优化主要用于提升程序的运行效率
-
静态提前编译器/AOT编译器
- 作用:直接把Java代码编译成本地机器码
- 代表:GNU Compiler for the Java(GCJ)、Excelsior JET
b.Java即时编译器与C/C++静态编译器的对比
- 即时编译器运行需要占用程序运行时间,使得优化手段受制于编译成本,否则用户将在启动程序察觉到重大延迟;而静态编译器的编译时间成本不是重点
- 静态编译器所有优化都在编译期完成,而即时编译器的动态性是把双刃剑,一方面要求虚拟机频繁进行动态检查从而消耗大量运行时间,而且难以全局优化、只能以激进优化来完成,另一方面拥有运行期性能监控的优化措施,如调用频率预测、分支频率预测、裁剪未被选择的分支等
- Java中使用虚方法的频率远大于C/C++,表示运行时对方法接收者进行多态选择的频率更大,因此在进行某些优化难度会更大
- Java在堆上进行对象的内存分配,而C/C++可在堆、栈上分配,减轻了内存回收的压力;且C/C++中主要由用户程序代码回收内存,不存在无用对象的筛选,相比于垃圾收集机制运行效率更高
2.早期(编译期)优化
几乎所有语言都提供一些语法糖来方便开发,或能提高效率、或能提升语法的严谨性、或能减少编码出错的机会,下面是几种常见语法糖:
-
泛型与类型擦除
- C#的泛型是真实泛型:无论在程序源码、编译后的IL、还是运行期的CLR中都是切实存在的,List<int>和List<String>在系统运行期生成,有自己的虚方法表和类型数据,属于不同的类型,这种实现称为类型膨胀
- Java的泛型是伪泛型:只在程序源码中存在,在编译后的字节码文件中就已替换为原生类型,并在相应的地方插入了强制转型代码,因此ArrayList<int>与ArrayList<String>是同一个类,这种实现称为类型擦除
- 自动装箱、拆箱
- 遍历循环
- 条件编译:使用条件为常量的if语句
3.晚期(运行期)优化
a.HotSpot虚拟机采用解释器与编译器并存的架构,交互情况:
- 当程序需要迅速启动和执行时,解释器可以先发挥作用,从而省去编译时间
- 程序运行后,随着时间的推移,编译器逐渐发挥作用,把更多代码编译成本地代码,从而获取更高的执行效率
- 如果程序运行环境受内存资源限制较大,可以用解释执行节约内存,反之可以用编译执行提升效率
- 解释器可作为编译器激进优化的逃生门,当激进优化不成立时,如加载新类后类型继承结构出现变化、出现罕见陷阱,可通过逆优化退回到解释状态继续执行。如图:
有上图可见,HotSpot虚拟机中内置了两个即时编译器:Client Compiler(C1编译器和)和Server Compiler(C2编译器),搭配模式:
- 混合模式(Mixed Mode):默认采用解释器与其中一个编译器进行配合工作,虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式和编译器,用户可以使用
-client
或-server
参数去强制指定虚拟机运行在Client模式或Server模式。- 解释模式(Interpreted Mode):使用参数
-Xint
,编译器不工作,都使用解释方式执行。- 编译模式(Compiled Mode):使用参数
-Xcomp
,优先采用编译方式执行,但解释器仍然要在编译无法进行的情况下介入执行过程。
b.HotSpot即时编译器的编译对象:热点代码
- 分类:
- 被多次调用的方法:采用JIT编译方式,以整个方法作为编译对象
- 被多次执行的循环体:采用OSR编译方式,发生在方法执行过程中,仍以整个方法作为编译对象
- 判断方式:通过热点探测
-
基于采样的热点探测(Sample Based Hot Spot Detection):周期性检查各个线程的栈顶,常出现在栈顶的方法就是热点方法
- 好处:实现简单、高效、易于获取方法调用关系
- 缺点:难以精确确认某个方法的热度、易受到线程阻塞或外界影响而扰乱热点探测
-
基于计数器的热点探测(Counter Based Hot Spot Detection):为每个方法建立计数器来统计方法的执行次数,执行次数超过一定的阈值就是热点方法
- 优点:精确、严谨
- 缺点:实现较麻烦、不能直接获取到方法的调用关系
- 计数器类型:
- 方法调用计数器(Invocation Counter):统计方法被调用的次数,当计数器超过阈值会触发JIT编译
- 回边计数器(Back Edge Counter):统计方法中循环体代码执行的次数,当计数器超过阈值会触发OSR编译
-
基于采样的热点探测(Sample Based Hot Spot Detection):周期性检查各个线程的栈顶,常出现在栈顶的方法就是热点方法
c.HotSpot即时编译器的编译过程
- Client Compiler:主要进行局部优化、放弃耗时较长的全局优化。采用简单快速的三段式编译:
- 第一个阶段:一个平台独立的前端把字节码构造成一种高级中间代码表示(HIR),在此之前会在字节码上完成一部分基础优化,如方法内联、常量传播等
- 第二个阶段:一个平台相关的后端从HIR中产生低级中间代码表示(LIR),在此之前会在HIR上完成另外一些优化,如空值检查消除、范围检查消除等,以便让HIR达到更高效的代码表示形式
- 第三个阶段:平台相关的后端使用线性扫描算法在LIR 上分配寄存器,并在LIR上做窥孔优化,然后产生机器代码。大致执行过程如图:
- Server Compiler:专门面向服务端的典型应用并且特别为服务端的性能配置调整过,是一个充分优化过的高级编译器,体现在:
- 会执行所有经典的优化动作:如无用代码消除、循环展开、循环表达式外提、消除公共子表达式、常量传播、基本块重排序等
- 会实施与Java特性密切相关的优化技术:如范围检查消除、空值检查消除等
- 根据解释器或Client Compiler提供的性能监控信息可能会进行一些不稳定的激进优化:如守护内联、分支频率预测等
另外,Server Compiler的寄存器分配器是一个全局图着色分配器,能够充分利用某些处理器架构上的大寄存器集合。虽然Server Compiler的编译时间比较缓慢,但是其编译速度远超于传统的静态优化编译器,且比Client Compiler编译输出的代码质量更高,能减少本地代码的执行时间,从而抵消了额外的编译时间开销。
d.HotSpot虚拟机即时编译器在生成代码时采用的代码优化技术:
其中几种最有代表性的优化技术:
- 语言无关的经典优化技术之一:公共子表达式消除(Common Subexpression Elimination)
- 含义:若一个表达式E已经计算过且E中所有变量值未发生任何变化,则称E为公共子表达式,此时没必要花时间再次计算,直接用之前计算过的表达式结果代替E即可
- 类型:
- 局部公共子表达式消除:优化仅限于程序的基本块内
- 全局公共子表达式消除:优化的范围涵盖了多个基本块
- 语言相关的经典优化技术之一 :数组边界检查消除(Array Bounds Checking Elimination)
- 若数组下标是个常量,只要在编译期根据数据流分析确定这个数组的长度,且判断得出该数组下标未越界,那么运行时无需再检查
- 若数组访问发生在循环中且使用循环变量来进行数组访问,只要在编译期根据数据流分析确定循环变量的取值范围永远在区间[0,数组长度)内,那么在整个循环中无需再进行多次检查
- 最重要的优化技术之一:方法内联(Method Inlining)
- 含义:把目标方法的代码复
- 制到发起调用的方法之中,避免发生真实的方法调用
- 主要目的:去除方法调用的成本,如建立栈帧等;为其他优化建立良好的基础,便于在更大范围上采取后续的优化手段、获取更好的优化效果
- 最前沿的优化技术之一:逃逸分析(Escape Analysis)
- 基本行为:分析对象动态作用域
- 类型:
- 方法逃逸:一个对象在方法中被定义后,可能被外部方法所引用。如作为调用参数传递到其他方法中
- 线程逃逸:一个对象在方法中被定义后,能被外部线程访问到。如赋值给类变量或可以在其他线程中访问的实例变量
- 对能够证明不会逃逸到方法或线程之外的对象可进行的优化手段:
- 栈上分配(Stack Allocation):在栈上对该对象进行内存分配,此时该对象所占用的内存空间会随栈帧出栈而销毁,可减少垃圾收集系统的压力
- 同步消除(Synchronization Elimination):在该对象上不会有读写竞争,可消除掉对该对象的同步措施,从而减少资源的消耗
- 标量替换(Scalar Replacement):若该对象可以进一步分解,那么直接创建它的若干个被这个方法使用到的成员变量来替换