优化

本篇介绍早期(编译期)优化晚期(运行期)优化
编译器篇::从编译器源码实现层次了解Java源码编译为字节码的过程,分析了Java语言中泛型、主动装箱拆箱、条件编译等语法糖。
运行期篇::着重介绍虚拟机的热点探测方法、HotSpot的即时编译器、编译触发条件,及如何从外部观察分析JIT编译的数据和结果,还选择了几种常见的编译器优化技术进行讲解。
Javac编译器只完成了从程序到抽象语法树或中间字节码的生成,因此称作“前端编译器”;在此之后,内置于虚拟机之内的“后端编译器”(JIT编译器)完成从字节码生成本地机器码的过程。
一、编译期优化
1.概述
编译过程中比较有代表性的编译器:
1)前端编译器:Sun的Javac、Eclipse JDT中的增量式编译器(ECJ)。(.java—>.class
2)JIT编译器:HotSpot VM的C1、C2编译器。—>虚拟机的后端运行期编译器 (字节码—>机器码)
3)AOT编译器:GNU Compiler for the Java(GCJ)、Excelior JET。—>静态提前编译器(.java—>机器码
性能的优化集中到后端即时编译器中(侧重程序运行),Javac做了很多针对编码过程的优化措施来改善编码风格和提高编码效率(侧重程序编码)。
2.Javac编译器
本身是由Java语言编写的程序。
2.1Javac的源码与调试
导入代码期间,可能会提示“Access Restriction”,被Eclipse拒绝编译(Eclipse的JRE System Library中默认包含了一系列的代码访问规则)。可通过添加一条允许访问Jar包中所有类的访问规则来解决。
这里写图片描述
编译过程大致可分为三个过程:
1)解析与填充符号表;
2)插入式注解处理器的注解处理;
3)分析与字节码生成。
2.2解析与填充符号表
2.2.1词法、语法分析
词法分析将源代码的字符流转变为标记(Token)集合;
语法分析根据Token序列来构造抽象语法树。此步骤之后,编译器基本不会再对源码文件进行操作,后续操作都建立在抽象语法树之上。
2.2.2填充符号表
符号表由一组符号地址和符号信息构成的表格。语义分析中,符号表所登记的内容将用于语义检查和产生中间代码。
2.3注解处理器
编译期间对注解进行处理,可以读取、修改、添加抽象语法树中的任意元素。如果在处理注解期间对语法树进行了修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止。
2.4语义分析与字节码生成
语法树无法保证源程序符合逻辑。语义分析主要任务是对结构上正确的源程序进行上下文有关性质审查,如进行类型审查。
2.4.1标注检查
语义分析过程分为标注检查和数据及控制流分析两个步骤。
标注检查步骤检查的内容包括:变量使用前是否已被声明、变量与与赋值之间的数据类型是否能够匹配等,另外还有一个重要的动作称为变量折叠
2.4.2数据及控制流分析
将局部变量声明为final,对运行期是没有影响的,变量的不变性仅仅由编译器在编译期间保证。
2.4.3解语法糖
Java中最常用的语法糖主要是:泛型、变长参数、自动装箱拆箱等。他们在编译阶段被还原为简单的基础语法结构,过程称为解语法糖
2.4.4字节码生成
字节码生成阶段不仅把前面各步骤生成的信息(语法树、符号表)转化成字节码写到磁盘中,编译器还进行了少量的代码添加和转换工作。实例构造器<init>() 和类构造器<clinit>() 就在此阶段添加到语法树之中(实例构造器并不指默认构造函数)
3.Java语法糖
3.1泛型与类型擦除
泛型本质是参数化类型的应用。可用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。

public class GenericTypes{
  public static void method(List<String> list){
    System.out.println("A");
  }
  public static void method(List<Integer> list){
    System.out.println("B");
  }  
}
//此段代码不能被编译,因为List<String>和List<Integer>编译后都被擦除了,变成了一样的原生类型List<E>.

擦除法所谓的擦除,仅仅对方法的Code属性中的字节码进行擦除,实际上元数据还是保留了泛型信息,因此可以通过反射手段取得参数化类型。
3.2自动装箱、拆箱与遍历循环

    public static void main(String[] args) {
        Integer a=1;
        Integer b=2;
        Integer c=3;
        Integer d=3;
        Integer e=128;
        Integer f=128;
        Long g=3L;

        System.out.println(c==d);//true
        System.out.println(e==f); //false :由于值>=128,
        System.out.println(e.equals(f));//true
        System.out.println(c==(a+b));//true
        System.out.println(c.equals(a+b));//true
        System.out.println(g==(a+b));//true
        System.out.println(g.equals(a+b));//false :包装类的 equals() 不能处理自动类型转换
    }
    //包装类的"=="运算在没有遇到算术运算时不会自动拆箱。

3.3条件编译

public static void main(String[] args){
   if(true){
     System.out.println("1");
   }else{
     System.out.println("2");
   }
}
//对上进行反编译结果
public static void main(String[] args){
     System.out.println("1");
}   
//只能使用条件为常量的语句才能达到上述效果。

只能实现语句基本块级别的条件编译。
二、运行期优化
1.概述
热点代码:虚拟机发现某个方法或代码块的运行特别频繁,就被认定。
为提高热点代码执行效率,虚拟机会在运行时把这些代码编译成与本地平台相关的机器码。并进行各种层次的优化,完成此任务的编译器称为即时编译器。
2.HotSpot虚拟机内的即时编译器
1)使用解释器与编译器并存架构;
2)实现了两个不同的即时编译器(C1: Client Compiler;C2: Server Compiler);
2.1解释器与编译器
程序需要快速启动和执行时,解释器可以首先发挥作用,省去编译的时间,立即执行。
程序运行后,随时间推移,编译器逐渐发挥作用,把越来越多的代码编译为本地代码后,获得更高的执行效率。
解释执行节约内存;编译执行提升效率
解释器可作为编译器激进优化时的“逃生门”,
主流的HotSpoy虚拟机默认采用解释器与其中一个编译器直接配合的方式工作(混合模式);会根据自身版本与宿主机器硬件性能自动选择运行模式。
-client:Client模式;
-server:Server模式。
-Xint:解释模式
-Xcomp:编译模式
即时编译器编译本地代码需占用程序运行时间
分层编译:在程序启动响应速度与运行效率之间达到最佳平衡。(JDK 1.7的Server模式作为默认编译策略被开启)
根据编译、优化的规模与耗时,划分出不同编译层次:
第0层:程序解释执行,解释器不开启性能监控功能(Profiling),可触发第1层编译。
第1层:也称C1编译,将字节码编译为本地代码,进行简单可靠的优化,如有必要将加入性能监控逻辑。
第2层:C2编译,将字节码编译为本地代码,会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。
实施分层编译后,Client Compiler和Server Compiler将会同时工作,许多代码都可能被多次编译。Client Compiler获取更高的编译速度,Server Compiler来获取更好的编译质量,解释执行时也无需再承担收集性能监控信息的任务。
2.2编译对象与触发条件
热点代码有两类:
1)被多次执行的方法;
2)被多次执行的循环体。(OSR:栈上替换)
热点探测
1)基于采样的热点探测:周期性检查各个线程的栈顶,若方法经常出现在栈顶,则为“热点方法”。热度不精确
2)基于计数器的热点探测(HotSpot虚拟机采用):为每个方法(甚至是代码块)建立计数器,若统计的执行次数超过阈值,则为:热点方法“。
方法调用计数器:Client模式下阈值为1500;Server模式下阈值为10000.
-XX:CompileThreshold:设定阈值;
热度衰减:当超过一定的时间限度(半衰周期),若方法的调用次数仍然不足以让它提交给即时编译器编译,那么这个方法的调用计数器值就会减少一半。
进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可用参数-XX:-UseCounterDecay关闭热度衰减,统计绝对次数。-XX:CounterHalfLifeTime:设置半衰周期,单位是秒。
回边计数器:统计方法中循环体执行次数,字节码中遇到控制流向后跳转的指令就称为”回边”目的是为了触发OSR编译。
使用-XX:OnStackReplacePercentage来间接调整回边计数器阈值。
1)Client模式:方法调用计数器阈值*OSR比率(OnStackReplacePercentage)/100.
OnStackReplacePercentage默认为933,阈值默认13995.
2)Server模式:方法调用计数器阈值*(OSR比率-解释器监控比率)/100.
OnStackReplacePercentage默认140,阈值默认10700.
超过阈值时提交一个OSR编译请求,并把回边计数器的值降低一些,以便继续在解释器中执行循环,等待编译器输出结果。
回边计数器统计的是方法循环执行的绝对次数,计数器溢出时,会把方法计数器的值也调整到溢出状态,下次再进入该方法时。就会执行标准编译过程。
这里写图片描述

这里写图片描述
2.3编译过程
-XX:-BackgroundCompilation参数可以禁止后台编译,当达到JIT的编译条件,执行线程向虚拟机提交编译请求后将会一直等待,直到编译过程完成后再开始执行编译器输出的本地代码。
后台执行编译过程中,编译器的工作:
这里写图片描述
2.4查看与分析即时编译结果
-XX:+PrintCompilation:要求虚拟机在即时编译时将被编译成本地代码的方法名打印出来;
-XX:+PrintInlining:要求虚拟机输出方法内联信息。
3.编译优化技术
介绍四种:
3.1公共子表达式消除
如果一个表达式E已被计算过,且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成为了公共子表达式。对这种表达式,没有必要再花时间对其计算。
分为局部公共子表达式消除和全局公共子表达式消除。
3.2数组边界检查消除
Java访问数组元素foo[i]时,系统会自动检查上下边界的范围,溢出则抛出java.lang.ArrayIndexOutOfBoundsException。但对虚拟机执行子系统来说,每次数组元素的读写隐含一次条件判断,也是一种性能负担。
!!数组下标是一个常量,如foo[3],只要在编译期根据数据流分析来确定foo.length的值,并判断”3”没有越界,执行时就无需判断了。
如果编译器只要通过数据流分析就可判定循环变量的取值范围永远在区间[0,foo.length]之内,那在整个循环中就可把数组的上下界检查消除掉,节省多次的条件判断操作。
3.3方法内联
除了消除方法调用的成本外,更重要的是为其他手段建立良好的基础。
除了使用invokespecial和invokestatic指令调用的方法,其他Java方法调用都需在运行时进行方法接收者的多态选择,并都有可能存在多于一个版本的方法接收者。Java语言中默认的实例方法就是虚方法
对虚方法,编译器做内联时无法确定应使用哪个方法版本。对非虚方法,直接内联即可,安全稳定。
为解决虚方法的内联问题,Java虚拟机设计团队首先引入“类型继承关系分析”(Class Hierarchy Analysis,CHA)技术。
若遇到虚方法,会向CHA查询此方法在当前程序下是否有多个版本,如果查询到只有一个版本,也可以内联,但属于激进优化,需预留一个“逃生门”(守护内联)。如果程序后续执行中加载了导致继承关系发生变化的新类,就需抛弃掉已经编译的代码,退回到解释状态执行,或重新编译。
如果向CHA查询出多个版本供选择,编译器还将会进行最后一次努力,使用“内联缓存”来完成方法内联。
3.4逃逸分析
逃逸分析基本行为就是分析对象动态作用域:当一个对象在方法中定义后,可能被外部方法所引用(例如,作为调用参数传递到其他方法中),这种行为称为“方法逃逸”。若被外部线程访问到(如:赋值给类变量或可在其他线程中访问的实例变量),称为“线程逃逸”。
若证明一个对象不会逃逸到方法或线程之外,可进行如下优化:
1)栈上分配
对象在栈上分配内存,而不在堆上分配。
2)同步消除
对变量的读写不会有竞争,对其同步措施可以消除。
3)标量替换
标量,指一个数据无法再分解成更小的数据来表示了。如果把一个Java对象(聚合量)拆散,根据程序访问情况,将其使用到的成员变量恢复原始类型来访问就叫“标量替换”。
对象被拆散后,可不创建对象,而直接创建它的若干个被这个方法使用到的成员变量来代替。拆分后,除可让对象的成员变量在栈上分配外,还可为后续进一步的优化手段创造条件。
目前虚拟机只能采用不那么准确,但时间压力相对较小的算法来完成逃逸分析。在HotSpot虚拟机中暂时还没有栈上分配这项优化。
无论哪种虚拟机模式,默认不开启逃逸分析
若确定对程序运行有益,可用参数-XX:+DoEscapeAnalysis手动开启逃逸分析,用-XX:+PrintEscapeAnalysis查看分析结果。
-XX:+EliminateAllocations:开启标量替换;
-XX:+PrintEliminateAllocations:查看标量替换情况。
+XX:+EliminateLocks:开启同步消除。
4.Java与C/C++编译器对比
其实就是“即时编译器”与“静态编译器”的对比。
即时编译器运行占用用户程序运行时间,有很大时间压力,提供的优化手段受制于编译成本。
Java语言是动态的类型安全语言,虚拟机需要频繁的进行动态检查,会消耗不少时间。
Java语言虚方法使用频率高,在运行时对方法接收者的多态选择频率远远大于C/C++。
Java语言是可以动态扩展的语言,运行时加载新的类可能改变程序类型的继承关系,使得很多全局优化难以进行,只能以激进优化方式完成。
Java语言中对象内存分配都在堆上进行,只有方法中的局部变量才能在栈上分配。而C/C++对象有多种内存分配方式,主要由用户程序代码来回收分配的内存,运行效率(没有说开发效率)比垃圾回收机制高。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值