• JVM编译器优化


      Java 语言的编译期大致可以分为三种:

    • 前端编译器,把.java 文件编译成class 文件—— .java -> .class
    • 后端运行期编译器 JIT 编译器(即时编译器),把字节码转变为机器码的过程—— .class -> 机器码
    • 静态提前编译器 AOT ,直接把 .java 文件编译成机器码——.java -> 机器码

      不同的编译时期,为了提高代码的运行效率,JVM 会进行一定的编译优化。

    1、早期优化

      早期编译主要是第一种编译,即把 .java 文件编译成 class 文件,这个过程的编译可以分为三个部分;

    • 解析与填充符号表
    • 插入式注解处理器的注解处理过程
    • 分析与字节码生成过程

    1.1 解析与填充符号表

      1.1.1 词法分析

        词法分析是将源代码的字符流转变为标记(Token)集合,一个标记就是一个关键字,变量名,字面量,或者运算符。

        比如:int a = b + 3; //这段代码包含6个标记,分别是int、a、=、b、+、3

      1.1.2 语法分析

        语法分析是根据Token序列构造抽象语法树的过程,抽象语法树是一种用来描述程序代码语法结构的树形表示方式,语法树的每个节点都代表着程序代码中的一个语法结构,例如包、类型、修饰器、运算符、接口、返回值甚至代码注释等都可以是一个语法结构。

      1.1.3 填充符号表

        完成词法分析和语法分析后,下一步就是填充符号表。符号表是由一组符号地址和符号信息构成的表格。

        符号表中的信息在编译的不同阶段都会用到。在语义分析中,符号表所登记的内容将用于语义检查和产生中间代码。在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的依据。

    1.2 注解处理器

      注解处理器可以读取,修改,添加抽象语法树中的任何元素。如果注解处理器在处理注解期间对语法树进行了修改,编译器将回到解析及填充符号表的过程重新开始,直到没有注解处理器对语法树进行修改为止,每次循环称为一个 Round。通过注解处理器之后,才可以干涉编译器的行为。

    1.3 语义分析与字节码分析

      编译器获得程序代码的抽象语法树后,语法树无法保证源程序是符合逻辑的。语义分析是对结构上正确的源程序进行上下文有关性质的审查。如类型审查。

      语义分析过程分为标注检查以及数据及控制流分析。

      1.3.1 标注检查

        标注检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配等。

        在标记检查的过程中还会进行常量折叠,如 int a = 1 + 2 ; //会折叠成 int a = 3; 不会增加CPU指令的运算量。

      1.3.2 数据及控制流分析

        对程序上下文逻辑进行更进一步的验证,它可以检查出局部变量是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理等问题。

      1.3.3 解语法糖

        语法糖能够增加程序可读性,从而减少程序代码出错的机会,对语言的功能没有影响。Java中常用的语法糖有泛型、自动装箱、自动拆箱、变长参数、遍历循环、条件编译等。

        解语法糖是指虚拟机运行时并不支持语法糖,语法糖在编译阶段会还原回简单的基础语法结构,并在相应的地方插入强制类型转换。

    • 泛型,java 语言中泛型实现方法称为类型擦除。
    • 自动装箱和自动拆箱,转换为对应的包装方法和还原方法。
    • 遍历循环,则把代码还原成了迭代器的实现。
    • 条件编译,对于 if while 等条件分支中不成立的代码块消除掉。
    • 变长参数,转换成一个数组类型参数。

      1.3.4 字节码生成

        字节码生成是javac编译过程的最后一个阶段。把前面的步骤生成的信息转化为字节码写到磁盘中并进行少量的代码添加和转换工作,比如实例构造器和类构造器。

    2、晚期优化(运行期优化)

      当虚拟机发现某个方法或代码块运行特别频繁时就会把这些代码认定为热点代码。为了提高热点代码的执行效率,在运行时,虚拟机会将这些代码编译成与本地平台相关的机器码,并进行各种层次优化,完成这个任务的编译器叫即时编译器(JIT)。

    2.1 解释器和即时编译器

      2.1.1 二者关系

      当程序需要迅速启动和执行的时候,解释器可以省去编译时间,立即执行,解释执行可以节约内存。 在程序运行的时候,编译器可以把越来越多的代码编译成本地代码,提高执行效率。解释器和编译器可以配合工作,可以从解释器转到即时编译器, 也可以从编译器逆优化到解释器。

      即时编译器 (JIT 编译器),可以分为 Client 编译器和 Server 编译器,简称 C1 编译器和 C2 编译器。JVM 会根据不同的平台选取不同的编译器,以实现效率的提高。

      如图:

     

     

      2.1.2 分层编译

        分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次。

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

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

        第2层,也成C2编译,将字节码编译为本地代码,会启用一些编译时较长的优化。

    2.2 编译对象

      2.2.1 热点代码

        热点代码有两类:(1)被多次调用的方法,编译时以整个方法做为编译对象,是标准的JIT 编译方式

                (2)被多次执行的循环体,以循环体所在方法为编译对象,因为发生在方法中,所以称为栈上替换编译(OSR 编译)

        判断是否为热点代码也有两种方式:

        (1)基于采样的热点探测:虚拟机会周期性的检查各个线程的栈顶,如果发现某个方法经常出现在栈顶,那这个方法就是热点方法。

        (2)基于计数器的热点探测:虚拟机为每个方法建立计数器,统计方法的执行次数,如果执行次数超过一定打阈值就认为是热点方法。

    2.3 编译过程

      2.3.1 Client Compiler编译流程

      对于 C1,编译的过程可分为三个阶段:

    • 第一阶段,一个平台独立的前端将字节码构造成高级中间代码表示(HIR),HIR使用静态单分配(SSA)的形式来代表代码值。在此之前编译器会在字节码上完成一部分优化,比如方法内联、常量传播等优化。
    • 第二阶段,一个平台的后端从 HIR 产生低级中间代码表示(LIR) 。在此之前会在HIR上完成另外一些优化,如空值检查消除、范围检查清除等。
    • 最后阶段,平台的后端使用线性扫描算法在 LIR 上分配寄存器,并在 LIR 上做窥孔优化,然后产生机器代码。

    流程如图 Client Compiler:

      2.3.2 Server Compiler编译流程

        Server Compiler是专门面向服务端的典型应用,它会执行所有经典的优化动作,如无用代码清除、循环展开、循环表达式外提、消除公共子表达式、常量传播、基本块重排序等。另外还会根据解释器或Client Compiler提供的性能监控信息,进行一些不稳定的激进优化,如守护内联、分支频率预测等优化(当激进优化的假设不成立,如类型继承结构出现变化时,可以通过逆优化退回到解释状态继续执行)。

    2.4 编译优化技术

      2.4.1 公共子表达式消除

        如果一个表达式 A 已经计算过了,并且从先前的计算到现在的 A 中所有的变量的值都没有变化,那么A的这次出现就成为了公共子表达式。对于这种表达式,没有必要花时间再对它进行计算,只需要直接用前面计算过的表达式结果代替A即可。比如:

     int a = b * c + (3 + b * c );
     //优化
     int a = A + (3 + A);
     //优化
     int a = 2 * A + 3;

      2.4.2 数组边界检查消除

        当访问一个数组的时候,经常会对数组的边界进行检查。而可以采取的优化就是在数据流分析时确定对数组的访问不会超过数组范围就不再每次进行数组越界检查。

      2.4.3 方法内联

        就是将一些没用的代码剔除,或者对于没有必要的方法跳转,将目标方法中的代码 “复制”到发起调用的方法之中,避免真实的方法调用。因为 Java 中的多态性的存在因此,内联有时不能确定目标方法,对应的情况如下:

        如果是非虚方法,直接进行内联。

        如果是虚方法,只有一个版本,进行守护内联属于激进优化,要设置”逃生门“,没有变化时继续内联,若是加载了一个有变化的新类就直接抛弃退回解释执行。如果有多个版本,就做内联缓存,在未发生内联的时候,缓存为空,第一次调用方法时,记录方法的调用者,后面每次调用就进行判断,一致就继续进行,不一致就取消内联。

      2.4.4 逃逸分析

        逃逸分析的基本行为就是分析对象的动态作用域:当一个对象在方法中被定义后可能被外部方法所引用,例如传参,称为方法逃逸。还有被外部线程访问到,例如赋值给类变量,称为线程逃逸。如果一个对象不会逃逸那么可以进行如下优化:

        (1)栈上分配:在栈上分配内存,对象所占的内存空间随栈帧出栈而销毁。
        (2)同步消除:消除没有必要的线程同步。
        (3)标量替换:标量是指一个数据不能再分解,比如原始数据类型(int、long等)。相对的,一个数据可以继续分解,那称它为聚合量,例如对象。如果把一个Java对象拆散,根据程序访问情况,将其使用到的成员变量恢复原始类型来访问,称为标量替换。如果一个对象可以被分解,且不会逃逸,就直接使用标量代替对对象的成员变量,而不直接创建这个对象。

     

  • 相关阅读:
    349. Intersection of Two Arrays
    1342. Reduce Array Size to The Half
    Telegram 汉化教程【转】
    jQuery 事件
    jQuery 事件
    jQuery 遍历
    jQuery 遍历
    CocosCreator教程(入门篇)【转】
    JavaScript shift() 方法使用【转】
    jQuery中动画函数animate的用法详解【转】
  • 原文地址:https://www.cnblogs.com/strong-FE/p/12146353.html
Copyright © 2020-2023  润新知