编程语言
首页 > 编程语言> > 10 程序编译与优化(个人理解,若有不足,敬请指出)

10 程序编译与优化(个人理解,若有不足,敬请指出)

作者:互联网

Java语言的“编译期”是一段不确定的过程,因为它可能指的是(1)前端编译器把java文件转变成class字节码文件的过程,也可能指的是虚拟机(2)后端运行期间编译器(JIT)把字节码转变成机器码的过程。更或者是(3)静态提前编译器直接将Java文件转换成机器码文件。

从JDK1.3开始,虚拟机设计团队就把对性能的优化集中到了后端的即时编译中,这样可以让那些不是由Javac产生的Class文件(如JRuby、Groovy等语言的Class文件)也能享受到编译期优化所带来的好处,Java在运行期的优化过程对于程序运行来说更重要,而前端编译期在编译期的优化过程对于程序编码来说关系。

在这里编译期优化指的是javac编译器将java文件转化为字节码的过程,而运行期间优化指的是JIT编译器所做的优化。

1——编译器优化

1.1解析与填充符号表过程;

解析步骤包含了词法分析和语法分析两个过程,首先词法分析是将源代码的字符流转变成为标记集合(token),就是将字符转换成一个集合,如int a = 7将转换成四个单独的token标记。token不可再拆分。
这一阶段是由com.sun.tools.javac.parser.Scanner实现

然后语法分析是根据token序列来构造抽象语法树(一种用来描述程序代码语法结构的树状表示方式),这一阶段由com.sun.tools.javac.parser.Parser实现。然后抽象语法树由com.sun.tools.javac.tree.JCTree类表示。后续编译器的操作都是在语法树之上的基础上执行。

完成词法分析和语法分析之后,下一步是填充符号表,符号表是由一组符号地址和符号信息构成的表格,符号表中所登记的信息在编译的不同阶段都要用到(比如语义分析中符号表所登记的内容将用于语义检查和产生中间代码,目标代码生成阶段当对符号名进行地址分配时,符号表是地址分配的依据)。该阶段是由com.sun.tools.javac.comp.Enter类实现。

1.2插入式注解处理器的注解处理过程

插入式注解处理器可以看做是一组编译器的插件,在这些插件里面,可以读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行了修改,那么编译器将回到解析及填充符号表的过程重新处理,直到所有的插入式注解处理器都没有再对语法树进行修改为止。

1.3语义分析与字节码生成过程。

语法分析之后,编译器获得了程序代码的抽象语法树表示,语法树能够表示结构正确的源程序的抽象,但是无法保证源程序是否符合逻辑,而语义分析主要是对结构上正确的源程序进行上下文有关性质的检查。

例子:
int a = 1;
Boolean b = ture;
char c = 2;
int d = a+c;
int e = b+c;
以上的代码都能正确的生成语法树,但是e这一行无法通过编译,不符合逻辑。这时我们就需要语义分析来辨别代码的正确与否。
1.3.1标注检查

标注检查步骤检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配,等等。还有一个重要的动作称为常量折叠(如int a =3+2;这里在语义分析变成a=5,减少运算量)也在此阶段完成。

1.3.2.数据及控制流分析

数据及控制流分析是对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否有返回值、是否所有的受查异常都被正确处理了等问题。
将局部变量声明为final,对运行期是没有影响的,变量的不变性仅仅由编译器在编译期间保障

1.3.3.解语法糖

语法糖是指在计算机语言中添加某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。java中的泛型,变长参数,自动拆箱与装箱,条件编译等就属于语法糖,它们在编译阶段就被还原成简单的语法结构(比如List和List在运行期间其实是同一个类)。

1.3.4.字节码生成

此过程是javac编译过程的最后一个阶段,字节码生成阶段将之前各个步骤所生成的信息转化成字节码写到磁盘中,另外还进行少量的代码添加和转换工作。

2——运行期优化

java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或者代码块的运行特别频繁时,就好把这些代码认定为“热点代码”,为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的诱惑,完成这个任务的编译器称为即时编译器(JIT编译器)。

2.1解释器和即时编译器

在整个虚拟机系统中,解释器和即时编译器经常配合工作。当虚拟机启动时,解释器可以首先发挥作用,而不必等待编译器全部编译完成再执行,这样可以省去许多不必要的编译时间。并且随着程序运行时间的推移,编译器逐渐发挥作用,根据热点探测功能,,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率。当然他和运行内存也有关,如嵌入式系统以解释器执行。

解释器铪作为逃生门一角色运行,如在即时编译器做一些有风险的激进优化失败时通过逆优化回退到解释状态执行(无解释器的虚拟机用c1担任逃生门)。

HotSpot中内置两个即时编译器C1编译器和C2编译器。可以通过“-client”和”server”参数去强制指定虚拟机运行在Client模式或Server模式。

解释器与编译器混搭配使用的方式在虚拟机中称为”混合模式”,“-Xint”强制虚拟机运行于“解释模式”,“-Xcomp”强制虚拟机运行于“编译模式”,但是解释器仍然要在编译无法进行的情况下介入执行过程,可以通过“-version”命令输出结果显示3中模式。

2.2分层编译

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

第1层:也称为C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要将加入性能监控的逻辑
第2层:也称C2编译,将字节码编译为本地代码,也会开启一些编译耗时较长的优化,升值会根据性能监控信息进行一些不可靠的激进优化。

使用分层编译后,二者共同工作,用Client Compiler(c1)获取更高的编译速度,用Server Compiler(c2)来获取更好的编译质量。

2.3编译对象和触发条件
在运行过程中会被即时编译器编译的“热点代码”有两类:

1.被多次调用的方法:由方法调用触发的编译,属于JIT编译方式

2.被多次执行的循环体:也以整个方法作为编译对象,因为编译发生在方法执行过程中,因此成为栈上替换(OSR编译)

热点探测判定方式有两种:

1.基于采样的热点探测:虚拟机周期性的检查各个线程的栈顶,如果某个方法经常出现在栈顶,则判定为“热点方法”。(简单高效,可以获取方法的调用关系,但容易受线程阻塞或别的外界因素影响扰乱热点探测)

2.基于计数的热点探测:虚拟机为每个方法建立一个计数器,统计方法的执行次数,超过一定阈值就是“热点方法”。(需要为每个方法维护计数器,不能直接获取方法的调用关系,但是统计结果精确严谨,hotspot采用第二种)

方法调用计数器

HotSpot中为每个方法准备了两类计数器:方法调用计数器和回边计数器。两个计数器都有一个确定的阈值,当计数器超过阈值时(就是该热点代码在二类计数器之和)就触发JIT编译。
方法调用计数器默认情况下CLient模式下是1500次,Server模式下是10000次,可以通过-XX:CompileThreshold来设置。一段时间内还未超过阈值,方法的调用计数器就会被减少一半,这种方法叫做计算器热度的衰减,这段时间称为次方法统计的半衰周期,进行衰减的动作是在虚拟机进行垃圾收集时顺便进行的。可以用参数-XX:-UseCounterDecay来关闭热度衰减。也可以通过参数设置半衰周期的时间。

回边计数器

回边计数器,统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”,建立回边计数器统计的目的就是为了触发OSR编译。

回边计数器阈值计算公式:Client:方法调用计数器阈值 X OSR比率/100其中OnStackReplacePercentage默认值是933,若都取默认值CLient模式虚拟机的回边计数器阈值为13995.Server:方法调用计数器阈值X(OSR比率-解释器监控比率)/100 其中OnStackReplacePercentage默认值是140,InterpreterProfilePercentage默认值是33,若都去默认值,阈值为10700

回边计数器没有技术热度衰减的过程,因此这个计数器统计的就是该方法循环执行的绝对次数,当计数器溢出的时候,他还会把方法计数器的值也调整到溢出状态,这样下次在进入该方法的时候会执行标准编译过程。

2.4 编译优化技术
2.4.1 公共子表达式消除

如果一个表达式E已经计算过,并且从先前到现在E中所有变量的值没有发生变化,那E的这次出现就成为了公共子表达式,对这种表达式没必要再花时间对它进行计算,只需要直接用前面计算过的表达式结构结果代替即可,如果这种优化仅限于程序的基本块内,便称为局部公共子表达式消除,如果覆盖了多个基本块则称为全局公共子表达式消除。如代码中a*b的代码出现后,若在后来代码执行过程中a,b值没有变化,以后出现的a *b或者b *a都可以用最开始的计算值E代替。

2.4.2 数组边界消除

在对数组元素的访问中会对元素进行范围检查,每一次读写都会去隐式做一次条件判断,对于大量访问数组的代码,这是较大的负担。那我们可以通过在编译器通过数据流确定其长度,然后判断我们访问的信息是否越界,然后不用在执行条件判断;循环中,我们也可以通过数据流分析循环变量是否在数组长度范围内节省条件判断。

2.4.3 方法内联

多个方法调用,执行时要经历多次参数传递,返回值传递及跳转等,C1采用方法内联,把调用到的方法的指令直接植入当前方法中。去除方法调用的成本,同时也为其他优化建立了良好的基础,因此各种编译器一般会把内联优化放在优化序列的最靠前位置,然而由于Java对象的方法默认都是虚方法,因此方法调用都需要在运行时进行多态选择,为了解决虚方法的内联问题,首先引入了“类型继承关系分析(CHA)”的技术。

1.在内联时,若是非虚方法,则可以直接内联

2.遇到虚方法,首先根据CHA判断此方法是否有多个目标版本,若只有一个,可以直接内联,但是需要预留一个“逃生门”,称为守护内联,若在程序的后续执行过程中,加载了导致继承关系发生变化的新类,就需要抛弃已经编译的代码,退回到解释状态执行,或者重新编译。

3.若CHA判断此方法有多个目标版本,则编译器会使用“内联缓存”,第一次调用缓存记录下方法接收者的版本信息,并且每次调用都比较版本,若一致则可以一直使用,若不一致则取消内联,查找虚方法表进行方法分派。

-XX:+PringInlining来查看方法内联信息,-XX:MaxInlineSize=35控制编译后文件大小。

2.4.4 逃逸分析

逃逸分析的基本行为就是分析对象动态作用域,当一个对象被外部方法所引用,称为方法逃逸;当被外部线程访问,称为线程逃逸。若能证明一个对象不会被外部方法或进程引用,则可以为这个变量进行一些优化:

1.栈上分配:如果确定一个对象不会逃逸,则可以让它分配在栈上,对象所占用的内存空间就可以随栈帧出栈而销毁。这样可以减小垃圾收集系统的压力。

2.同步消除:线程同步相对耗时,如果确定一个变量不会逃逸出线程,那这个变量的读写不会有竞争,则对这个变量实施的同步措施也就可以消除掉。

3.标量替换:如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那么程序真正执行的时候可以不创建这个对象,改为直接创建它的成员变量。

标签:10,编译,优化,虚拟机,计数器,敬请,编译器,方法
来源: https://blog.csdn.net/weixin_38287519/article/details/99087165