实战java虚拟机12-程序编译与优化

实战java虚拟机
深入理解java虚拟机

java语言的“编译期”其实是一段“不确定”的操作过程,

  • 因为它可能是指一个前端编译器把*.java文件转换成*.class文件的过程;例如:Sun的javac,Eclipse JDT中的增量式编译器(EJC)
  • 也可能是指虚拟机的后端运行期编译器(JIT编译器,Just In Time Compiler)把字节码转换为机器码的过程;例如:HotSpot VM的 C1,C2编译器
  • 还可能是使用静态提前编译器(AOT编译器,Ahead of Time Compiler)直接把*.java文件编译成本地机器代码的过程。

因为javac这类编译器对代码的运行效率几乎没有任何优化措施,虚拟机团队把对性能的优化集中到了后端的即使编译器(JIT)中,这样也能让那些不是由javac产生的class文件也同样能享受到编译器优化带来的好处.
但是javac做了许多针对java语言编码过程的优化措施来改善程序员的编码风格和提高编码效率。相当多新生的java新特性,都是靠编译器的语法糖来实现,而不是依赖虚拟机底层来支持。
总的来说:
前端编译器(javac)在编译期的优化过程对程序编码来说关系更加密切。
即时编译器(JIT)在运行期的优化过程对于程序运行来说更重要。

早期(编译期)优化

泛型与类型擦除
泛型是JDK1.5的一项新增特性,它的本质是参数化类型(Parametersized Type)的应用,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别成为泛型类,泛型接口,泛型方法。
java语言中的泛型规则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type,也成为裸类型),并在相应的地方插入了强制转型代码。因此,对于运行期的java语言来说,ArrayList<Integer>和ArrayList<String>是同一个类,所以泛型技术实际上是java语言的一颗语法糖,java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型
源代码

public class ParametersizedClear {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<String,String>();
        map.put("hello", "你好");
        map.put("kitty", "凯迪");

        System.out.println(map.get("hello"));
    }
}

编译后的class文件,再用字节码反编译工具进行反编译后代码:

public class ParametersizedClear
{
  public static void main(String[] args)
  {
  //类型擦除
    Map map = new HashMap();
    map.put("hello", "你好");
    map.put("kitty", "凯迪");

    //强转为String
    System.out.println((String)map.get("hello"));
  }
}

当泛型遇到重载

public class GenericOverload {
    public void m1(List<Integer> list) {
    }

    public void m1(List<String> list) {
    }
}

上述代码编译出错,是因为编译之后类型被擦除了,变成了一样的原生类型List<E>.


条件编译

public static void main(String[] args) {
    if(true){
        System.out.println("true");
    }else {
        System.out.println("false");
    }
}

编译结果为:

public static void main(String[] args) {
     System.out.println("true");
 }

类似的:

public static void main(String[] args) {
    if(false){
        System.out.println("false");
    }
}

编译后的结果:

  public static void main(String[] args) {
   }

编译时计算
如果程序中出现了计算表达式,如果计算表达式能够在编译时确定,那么表达式的计算会提前到编译阶段,而不是在运行时计算。
比如很多时候,为了增强代码的可读性,往往不会把最终的数值写到代码中,通常倾向于把计算过程写在代码中:

for (int i = 0 ; i < 60 * 60 * 24 ; i++){
    // do sth
}

看到这段代码,可能会给人产生一种怀疑,是不是每次循环都要计算一次呢?查看这段代码生成的字节码如下:

0: iconst_0
1: istore_1
2: iload_1
3: ldc           #2                  // int 86400
5: if_icmpge     14
8: iinc          1, 1
11: goto          2
14: return

Switch语句的优化
对于switch语句,编译器会产生两种字节码指令:tableswitch和lookupswitch.
其中tableswitch效率高于lookupswitch,但tableswitch只能处理case情况是连续的数值,而lookup可以处理不连续的情况。一般来说,总是希望可以尽可能的使用tableswitch.

public void switchMethod(int i) {
    switch (i){
        case 1: break;
        case 2: break;
        case 5: break;
        default:
            System.out.println("default");
    }
}

使用javap查看生成的字节码:

0: iload_1
 1: tableswitch   { // 1 to 5
               1: 36
               2: 39
               3: 45 ####填充了case3,4 跳转到default
               4: 45 ####
               5: 42
         default: 45
    }
36: goto          53
39: goto          53
42: goto          53
45: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
48: ldc           #4                  // String default
50: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
53: return

可以看到,为了使用tableswitch,编译器将离散的数据进行了填充,其中case3,4都跳转到了default


晚期(运行期)优化

java程序最初是通过解释器(Interpreter)进行解释的,当虚拟机发现某个方法或者代码块的运行特别频繁时,就会把这些代码认定为热点代码(Hot Spot Code).为了提高代码的执行效率,在运行时,虚拟机将会把这些代码编译成与平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler,JIT编译器

开启JIT编译器
java虚拟机有3种执行方式,分别是解释执行、混合模式和编译执行。默认情况下处于混合模式。
这里写图片描述

  • 使用java -Xint -version命令,开启解释执行模式,所有的代码均解释执行,不做任何JIT编译。
  • 使用java -Xcomp -version命令,开启编译模式,对于所有函数,无论是否是热点代码,都会被编译执行。

JIT编译阈值
默认情况下,JIT使用混合模式运行(指定参数为-Xmixed),在混合模式,只会对热点代码进行即时编译。对于是否为热点代码,虚拟机内有一个阈值进行判断,当函数调用次数超过这个阈值时,就被认为是热点代码,进行即时编译。
这里写图片描述

在Client模式,这个阈值时1500次。在Server模式下阈值为10000次。使用参数-XX:CompileThreshold来设置这个阈值,使用参数-XX:+PrintCompilation可以打印出即时编译的日志。

多级编译器
java虚拟机中拥有客户端编译器和服务端编译器两种编译系统,一般称为C1和C2编译器。使用-client 参数时,虚拟机会使用C1编译器,使用-server参数时,会使用C2编译器。

C1编译器的特点是编译速度快,C2的特点是会做更多的编译时优化,因此编译时间会长于C1,但是编译后的代码质量会高于C1.

为了使C1和C2在编译速度和执行效率之间取得一个平衡,虚拟机支持一种叫多级编译的策略:

  • 0级(解释执行):采用解释执行,不采集性能监控数据。
  • 1级(简单的C1编译):采用C1编译器,进行最简单的快速编译,根据需要采集性能数据。
  • 2级(有限的C1编译):采用C1编译器,进行更多的优化编译,可能会根据第1级采集的性能数据,进一步优化编译代码
  • 3级(完全的C1编译):完全使用C1编译器,会采集性能数据进行优化
  • 4级(C2编译):完全使用C2编译器,进行完全的优化

要使用多级编译器可以使用参数-XX:+TieredCompilation打开多级编译器的策略,且必须在使用-server模式启动才有效果。


公共子表达式消除
公共子表达式消除是一个普遍应用于各种编译器的经典优化技术。它的含义是:如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有变量的值没有发生变化,那么E的这次出现就成为了公共子表达式。对于这种表达式,没有必要花时间再对它进行计算,只需用E替换即可。假设如下代码:

int d = (c * b) * 12 + a + (a + b * c);

//表达式可能被视为
int d = E * 12 + a + (a + E);

//编译器还可能进行另一种优化
int d = E * 13 + a * 2;

方法内联
方法内联是编译器最重要的优化手段之一,除了消除方法调用的成本之外,它更重要的意义是为其他优化手段建立良好的基础。JIT编译器默认会打开内联优化。
例:

public class InLineMain {
    static int i = 0 ;

    static void inc(){
        i++;
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for(int j = 0 ; j < 1000000; j++) {
            inc();
        }

        System.out.println((System.currentTimeMillis() - start));
    }
}

方法内联之后,循环体会变成:

for(int j = 0 ; j < 1000000; j++) {
    i++;
}

使用命令:-server -XX:+InLine,打开内联优化,输出耗时;
使用命令:-server -XX:-InLine,关闭方法内联优化。

内联的优化效果非常显著,但是它会增大系统执行代码的体积,因此对于大的方法体,使用内联需要谨慎。虚拟机提供了一些参数来控制对于多大的代码允许内联
-XX:FreqInLineSize可以控制热点方法进行内联的体积上限。

逃逸分析
逃逸分析

猜你喜欢

转载自blog.csdn.net/it_freshman/article/details/81206559