java编译器优化和运行期优化

概述

   最近在看jvm优化,总结一下学习的相关知识

(一)javac编译器

编译过程

1、解析与填充符号表过程

1)、词法、语法分析

   词法分析将源代码的字符流转变为标记集合,单个字符是程序编写过程的最小元素,而标记则是编译过程的最小元素,javac中由com.sun.tools.javac.parser.Scanner类实现
   语法分析是根据token序列构造抽象语法树的过程。抽象语法树(AST)是一种用来描述程序代码语法结构的树形表示方式,语法树种的每一个节点都代表着程序代码中的语法结构,javac中,语法分析过程由com.sun.tools.javac.tree.parser.Parser类实现,这个阶段产生出的抽象语法树由com.sun.tools.javac.tree.JCTree类表示

2)、填充符号表

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

2、插入式注解处理器的注解处理过程

   注解在运行期间发挥作用,通过插入式注解处理器标准API中可以读取、修改、添加抽象语法树种的任意元素,若在处理注解期间对语法树进行修改,编译器将回到解析即填充符号表的过程重新处理,知道所有插入式注解处理器都没有再对语法树进行修改为止,每一次循环称为一个round。javac源码中插入式注解处理器的初始化过程是在initProrcessAnnotation()方法中完成的,而它的执行过程则是在processAnnotation()方法中完成。

3、分析与字节码生成过程

1)、标注检查

   attribute()方法,标注检查步骤检查的内容包括诸如变量使用前是否已经被声明、变量与赋值之间的数据类型是否够匹配以及常量折叠。javac中实现类是com.sun.tools.javac.comp.Attr类和com.sun.tools.javac.comp.Check类

2)、数据及控制流分析

   flow()方法,对程序上下文逻辑更进一步的验证,他可以检查出诸如程序局部变量在使用前是否赋值、方法的每条路径是否都有返回值、是否所有的受检查异常都被正确处理了问题
   局部变量在常量池中没有CONSTANT_Fieldref_info的符号引用,自然没有访问标志的信息,甚至可能连名称都不会保存下来
   将局部变量声明为final,对运行期是没有影响的,变量的不变性仅仅由编译器在编译期间保障

3)、解语法糖

   也称糖衣语法,指在计算机中添加某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用,通常来说,使用语法唐能够增加程序的可读性,从而减少程序代码出错的机会。java中最常用的是泛型、变长参数、自动装箱/拆箱等

4)、字节码生成

   javac编译的最后一个阶段,javac源码里面由com.sun.tools.javac.jvm.Gen类来完成,这个阶段不仅仅把前面各个步骤所生成的信息转化成字节码写到磁盘中,编译器还进行少量的代码添加转换工作

   保证一定是按先执行父类的实例构造器,然后初始化变量,最好执行语句块的顺序进行

javac语法糖

1、泛型与类擦除

   泛型类、泛型接口、泛型方法,C#中List与List就是两个不同的类型,他们在系统运行期生成,有自己的需方发表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型。

   java中泛型不一样,他只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型,并在相应得地方插入了强制转换代码,对于运行期的java来说ArrayList与ArrayList就是同一个类,java语言中的泛型实现方法称为类型擦除,基于这种方法的叫伪泛型

   在Class文件格式中,只要描述符不是完全一致的两个方法就可以共存,也就是说两个方法如果有相同的名称和特征签名,但返回值不同,那他们也是可以合法地共存于一个class文件中

   Signature是解决伴随泛型而来的参数类型的识别问题中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名,这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息、

   擦除法所谓的擦除,仅仅是对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能够通过反射手段取得参数化类型的根本依据。

2、自动装箱、拆箱与循环遍历

3、条件编译

   java编译器并非一个个的编译Java文件,而是将所有编译单元的语法树顶级节点输入到待处理列表后再进行编译,因此各个文之间能够互相提供符号信息。

   java中根据布尔常量值的真假,编译器会把分支中不成立的代码块擦除掉。这一工作将在编译器解除语法糖阶段完成。

4、常用语法糖

   泛型、自动装箱、自动拆箱、遍历循环、变长参数、条件编译、内部类、枚举类、断言语句、对枚举、字符串的switch,try与居住定义和关闭自由。

(二) 运行期

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

HotSpot虚拟机内的即时编译器

1) 解释器与编译器

   当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行,在程序运行后,随着时间的腿与,编译器逐渐发挥作用,吧越来越多的代码编译成本地代码之后可以获得更高的执行效率。当程序运行环境中内存资源限制较大,可以使用解释执行节约内存,反之可以使用编译执行来提升效率,

   解释器还可以作为编译器激进优化时的一个“逃生门”,让编译器根据概率选择一些大多数时候都能提升运行速度的优化手段,当激进优化的假设不成立,如加载了新类后类型继承结构出现变化、出现“罕见陷阱”时可以通过逆优化退回到解释状态继续执行,部分没有解释器的虚拟机中采用不进行激进优化的C1编译器担任“逃生门”的角色。

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

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

分层编译

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

   第1层:也称为C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要将加入性能监控的逻辑

   第2层:也称C2编译,将字节码编译为本地代码,也会开启一些编译耗时较长的优化,升值会根据性能监控信息进行一些不可靠的激进优化。

   用Client Compiler获取更高的编译速度,用Server Compiler来获取更好的编译质量。

2) 编译对象与触发条件

   在运行过程中会被即时编译器编译的“热点代码”有两类,即:被多次调用的方法,被多次执行的循环体。

   第一种情况:由于方法调用触发的编译,因此编译器理所当然地会以整个方法作为编译对象,这种编译也是虚拟机中标准的JIT编译方式。

   后一种情况,锦官编译动作是由循环体所触发,但编译器依然会以整个方法作为编译对象,称为“栈上替换–简称OSR编译”

   热点探测:1、基于采样的任店探测,采样这种方法的虚拟机会周期性地检查各个线程的栈顶,若发现某个方法经常出现在栈顶,则为热点方法,基于采样的热点探测好处是实现简单、高效,缺点是很难精确地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测
2、基于计数器的热点探测,虚拟机为每个方法建立计数器,统计方法执行次数,若超过阈值则认为为热点方法。虽然麻烦些但是统计结果更加精确和严谨。

   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

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

3) 编译过程

   虚拟机在代码编译器未完成之前,都仍然将按照解释方式继续执行,而编译动作则在后台的编译线程中进行。也可以禁止后台编译,禁止后,一旦达到JIT的编译条件,执行线程向虚拟机提交编译请求后将会一直等待,知道编译过程完成后在开始执行编译器输出的本地代码。

   Client Compiler模式下是一个三段式编程,第一阶段,一个平台独立的前端将字节码构造成一种高级中间代码表示(HIR),HIR使用静态但分配的形式来代表代码值,这可以使得一些在HIR的构造过程之中和之后进行的优化动作更容易实现,在此之前编译器会在字节码上完成一部分基础优化,如方法内联、常量传播等。优化将会在字节码被构造成HIR之前完成
   第二阶段,一个平台相关的后端从HIR中产生低级中间代码表示LIR.
   最后是在平台相关的后端使用线性扫描算法在LIR上分配寄存器,并在LIR上做窥孔优化,然后产生机器代码。

   Server Compiler则是专门面向服务端的典型应用并为服务端的性能配置特别调整过的编译器,也是一个充分优化过的高级编译器,他会执行所有经典的优化动作,如:无用代码消除,循环展开、循环表达式外提、消除公共子表达式、常量传播、基本块重排序等,还会实施一项与java语言特性密切相关的优化技术,如范围消除、控制检查消除。

   Server Compiler的寄存器分配器是全局图着色分配器,从即时编译器来看他无疑是比较缓慢的,但他的编译速度依然远远超过传统静态优化编译器,而相对于Client Compiler编译输出的代码质量有所提高,可以减少本地代码执行时间,

4) 查看及分析即时编译结果

   

编译优化技术

1) 概述

   对代码的所有优化措施都集中在即时编译器中,即时编译器产生的本地代码会比Javac产生的字节码更加优秀

   方法内联、冗余访问消除、覆写传播、无用代码消除。

2) 公共子表达式消除

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

   

3) 数组边界检查消除

   自动装箱消除、安全点消除、消除反射等。

4) 方法内联

   只有使用invokespecial指令调用的私有方法、实例构造器、父类方法以及使用invokestatic指令进行调用的静态方法才是在编译器进行解析的,其他java方法都需要在运行时进行方法接收者的多态选择。java语言默认的实例方法是虚方法。

   激进优化,需要留一个逃生门,称为守护内联。内联缓存来完成方法内联。

5) 逃逸分析

   分析对象动态作用域,当一个方法被定以后,它可能被外部方法所引用,称为方法逃逸,甚至还有可能被外部线程访问到,称为线程逃逸。

   若能证明一个对象不会逃逸到方法或线程之外,这可以通过栈上分配、同步消除、标量替换来进行优化。在一般应用中不会逃逸的局部对象所占比例很大,若能栈上分配就会随着方法的结束而自动销毁了,垃圾回收系统的压力将会小很多。如果一个数据可以继续分解,则称它为聚合量。对于一个对象,将其使用到的成员变量恢复原始类型来访问就叫做标量替换。

java与C/C++对比

   1、即时编译器运行占用的是用户程序的运行时间,具有很大的时间压力,他能提供的优化手段也严重受限于编译成本

   2、java语言是动态的类型安全语言,这就意味着要由虚拟机来确保程序不会违反语言语义或访问非结构化内存。

   3、java中没有virtual关键字,但是使用虚方法的频率确远远大于C/C++,这就意味着运行时对方法接受者进行多态选择的频率要远远大于后者,也就意味着即时编译器在进行一些优化时难度要远远大于C、C++的静态优化编译器。
4、java语言是可用动态扩展的语言,运行时加载新的类可能改变程序类型的继承关系,这使得很多全局的优化都难以进行。

5、java语言中对象的内存分配都是在堆上分配,只有方法中的局部变量才能在栈上分配,而c、c++对象则有多种内存分配方式,可能在堆上分配,也可能在栈上分配,

动态安全、动态扩展、垃圾回收这些特性为java开发效率做了很大功效。
c、c++编译器所有优化都是在编译器完成,以允许期性能监控为基础的优化措施方案都无法进行,如调用频率预测、分支频率预测、裁剪为被选择的分支等。

你可能感兴趣的:(java)