Java代码优化技术 编译期优化 - 前期优化,运行时优化 - 后期优化

优化

分为编译期优化和运行时优化

编译期优化

前端编译:字节流 -> 抽象语法树 -> jvm规范class字节码

javac优化:语法糖:在编译期解语法糖。

泛型及其擦除,自动拆装箱,Foreach循环,变长参数,条件编译,内部类,枚举类,断言语句,对字符串的switch支持,字符串加法

  • 泛型

    java中的泛型只存在于源码中,会在编译期间替换为原生类型,在字节码中已经不存在泛型,所以泛型技术实际是java的语法糖,被称为伪泛型。

  • 变长参数

    其本质为数组类型的参数,在编译期解语法糖。

    public void print(String ... args) {
        for (int i = 0; i < args.length; i++) {
            out.println(args[i]);
        }
    
  • 条件编译

    将分支中不成立的代码块消除掉。

运行时优化技术

解释器 + 即时编译器

扫描二维码关注公众号,回复: 5740336 查看本文章

此过程占用用户时间(如果耗时较长,用户将感受到延迟),所以不能大规模的将全部代码进行C2编译优化。

  • 即时编译器Just In Time JIT,把高频次使用的代码编译生成本地代码,获得更高的执行效率

    热点代码:被多次执行的方法 + 被多次执行的循环体

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

    方法调用计数器,越热的代码越高等级的编译优化

    • 解释执行

      方法调用计数器触发JIT编译

    • C1编译 简单可靠的优化

    • C2编译 耗时较长,较为激进(多态缓存的方法内联等)的优化 执行效率非常高

  • 解释器省去编译时间,立即执行;作为编译器激进优化出错(小概率)的逃生门

**记:**内联公式,数组逃逸

  • 方法内联

    将目标方法的代码复制到发起调用的方法中,避免发生真实的方法调用,消除方法调用的成本,为其他优化手段建立良好的基础。

    Java语言默认的实例方法为虚方法,对于虚方法来说,Java只有在运行时才能确定到底要调用方法的哪个版本.在编译器的方法内联过程中,根本就无法完成.为了解决虚方法的内联问题,Java虚拟机引入"类型继承关系分析"(Class Hierarchy Analysis, CHA)技术,用于确定在目前已加载的类中,某个接口是否有多于一种的实现,某个类是否存在子类,子类是否为抽象类等信息.

    • 编译器在进行内联时,如果是非虚方法,那么直接进行内联就可以了,这时候的内联是有稳定前提保障的.

    • 如果遇到虚方法,则会向CHA查询此方法在当前程序下是否有多个目标版本可供选择

      • 如果查询结果只有一个版本,那也可以进行内联,不过这种内联就属于激进优化,需要预留一个"逃生门",称为守护内联.如果程序的后续执行过程中,虚拟机一直没有加载到会令这个方法的接受者的继承关系发生变化的类,那这个内联优化的代码就可以一直使用下去.但如果加载了导致继承关系发生变化的新类,那就需要抛弃已经编译好的版本,退回到解释状态执行,或者重新进行编译.

      • 如果向CHA查询出来的结果是有多个版本的目标方法可供选择,则编译器还将会进行最后一次努力,使用内联缓存来完成方法内联,这是一个建立在目标方法正常入口之前的缓存,它的工作原理大致是:在未发生方法调用之前,内联缓存状态为空,当第一次调用发生后,缓存记录下方法接受者的版本信息,并且每次进行方法调用时都比较接受者版本,如果以后进来的每次调用的方法接受者版本都是一样的,那这个内联还可以一直用下去.如果发生了方法接受者不一致的情况,就说明程序真正使用了虚方法的多态特性,这时才会取消内敛,查找虚方法表进行方法分派.

        类似偏向锁的处理方法。

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

    例子:
    int d = c*b*12 + a + b*c

    int d = E*12 + a + E, E = b*c

  • 数组范围检查消除

    由于Java在访问数组时,会有一个隐含的越界检查,这个操作会一定程度上降低效率.

    如果数组的下标是一个常量,如foo[3],只要在编译器根据数据流分析来确定foo.length的值,并判断下标"3"没有越界,执行的时候就无须判断了.更加常见的情况是数组访问发生在循环之中,并且使用循环变量来进行数组访问,如果编译器只要通过数据流分析就可以判定循环变量的取值范围永远在[0, foo.length)之内,那在整个循环中就可以把数组的上下界检查消除,这可以节省很多次的条件判断操作.

  • 逃逸分析

    逃逸分析的基本行为就是分析对象动态作用域。

    • 方法逃逸
      当一个对象在方法中被定义后,他可能被外部方法所引用,例如作为调用参数传递到其他方法中
    • 线程逃逸
      甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量

    如果能证明一个对象不会逃逸到方法或线程之外,也就是别的方法或线程无法通过任何途径访问到这个对象,则可能为这个变量进行一些高效的优化。如:

    **记:**针对方法逃逸,进行对象的栈上分配和标量消除,针对线程逃逸进行同步消除。

    • 栈上分配

      如果确定一个对象不会逃逸出方法之外,那让这个对象在栈上分配内存(而不必分配到堆上,通过GC过程完成回收),对象所占用的内存空间就可以随栈帧出栈而销毁.

      在一般应用中,不会逃逸的局部变量所占的比例很大,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,垃圾回收系统的压力将会小很多.

    • 同步消除

      线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以消除掉.

    • 标量替换

      使用标量替换掉聚合量。

      • 标量:指一个数据已经无法再分解为更小的数据来表示了.Java虚拟机中原始数据类型(int,long等数值类型以及reference类型等)都不能再进一步分解,它们就可以称为标量.
      • 聚合量:如果一个数据可以继续分解,那么它就称作聚合量,Java中的对象就是最典型的聚合量.

      如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。

猜你喜欢

转载自blog.csdn.net/whichard/article/details/87873220