• JVM 运行期优化 & 反射优化


    即时编译

    分层编译
    public class Test {
       
    	// -XX:+PrintCompilation 会打印出java文件编译后的样子  -XX:-DoEscapeAnalysis 是否进行逃逸分析,“-”就是关掉。
    	public static void main(String[] args) {
       
    		for (int i = 0; i < 200; i ++) {
       
    			long start = System.nanoTime();
    			for(int j = 0; j < 1000; j ++) {
       
    				new Object();
    			}
    			long end = System.nanoTime();
    			System.out.printf("%d\t%d\n", i, (end - start));
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    这个例子分了两层循环,内层循环创建1000个Object对象,外层循环对内层循环进行计时统计,看每次创建1000个对象耗费多长时间。运行如下:

    0	66560
    1	49066
    2	45227
    3	49067
    4	58880
    5	58880
    6	55040
    7	52053
    8	49920
    9	46933
    10	45226
    ...
    68	18774
    69	18346
    ...
    145	61867
    146	853
    147	854
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    前面的是外层循环的次数,后面的数字是内层循环创建1000个对象耗费的时间。可以看到前面的有些循环是5开头甚至6开头,大约在第67次循环时,这个速度好像更快了,以2或1开头了,大约到146次时,速度一下子降到了三位数。这是因为,在运行期间,Java虚拟机会对我们的代码做一定的优化。

    实际上,JVM把字节码执行的执行状态分成了5个层次:

    • 0层,解释执行(Interpreter)。即字节码被加载到虚拟机以后,会靠一个解释器去一个字节一个字节的去解释,解释器会把我们的字节码解释为真正的机器码,这样CPU就能识别并执行了。需要注意的是,当你的字节码被反复调用时,他认为你这种字节码是反复使用的,那么到达一定的阈值以后,他就会启用编译器对这个字节码进行编译执行。就是从0层会上升到1层。
    • 1层,使用C1即时编译器编译执行(不带profiling)。编译器分两种,一种是“C1即时编译器”,第二种是“C2即时编译器”,C1即时编译器占据了1~3层,C2即时编译器是第4层。那么即时编译器和解释器有什么不同呢?即时编译器就是把这些反复执行的一些代码编译成机器码,然后存储在一个code cache(代码缓存)当中,那么下次再遇到相同代码时,就不会把每个字节码再来解释为机器码,而是把编译好的机器码直接拿出来用,这样效率肯定比你逐行解释高。C1和C2的区别就是他们的优化程度不一样,C1你可以认为他制作一些最基本的优化,C2做一些更完全更彻底的优化,但C1比C2多了一个他就是要进行一些profiling这样的信息统计操作。profiling就是指在代码运行期间,他会收集这些字节码运行的状态的数据,比如你如果是方法的话那“这个方法调用了多少次”,如果是循环的话“循环了多少次”。这些都是在C1编译器里做一些基本的统计,统计发现你某个方法被频繁的调用了,那他就会针对这个方法再上升为C2编译器,让C2编译器对他进行更完全更彻底的优化。
    • 2层,使用C1即时编译器编译执行(带基本的profiling)。
    • 3层,使用C1即时编译器编译执行(带完全的profiling)。
    • 4层,使用C2即时编译器编译执行。

    即时编译器(JIT)与解释器的区别:

    • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释。
    • JIT是将一些热点字节码编译为机器码,并存入code cache,下次遇到相同的代码,直接执行,无序再编译。
    • 解释器是将字节码解释为针对所有平台都通用的机器码。
    • JIT会根据平台类型,生成平台特定的机器码。(因为代码都运行起来了,所以肯定确定了平台,所以他会进行更激进的更彻底的优化

    需要注意的是,其实有的时候很多代码或只会调用一次,很多方法也只会调用一次,那如果把这些每次编译成机器码,其实这个编译本身也是要耗费时间的,所以JVM采用的是接下来的策略:对于占据大部分的不常用的代码,没必要耗费时间将其编译成机器码,因为这些代码执行的频率很少,所以采取解释执行的方式运行;另一方面,如果对于仅占据小部分的热点代码,比如方法调用次数或循环次数达到一定的阈值,我们则可以将其编译成机器码,以达到比较理想的运行速度。

    运行效率上简单比较一下,Interpreter < C1 < C2,C1的效率可以提升到五倍左右,C2的执行效率可以提升到10~100倍,所以即时编译器总的目标就是去发现热点代码(这也是为什么oracle的虚拟机叫hotspot,hotspot就是热点的意思),并加以优化。

    上面例子中使用的优化手段叫“逃逸分析”,所谓逃逸分析就是比如在上面代码中,他去分析new Object对象会不会在循环外面会用到,或会不会被其他方法所引用,结果发现不会(本例子中),因为它就是循环内的局部变量,所以他就采用了这种优化手段,即他发现创建对象的操作不会逃逸,即意味着外层不会用到这个对象,既然不会用到我干嘛创建你呢,所以可以看到刚才他的执行时间突然变短的原因是因为JIT进行了逃逸分析以后,当然这个逃逸分析是在C2编译器里做的优化,会把这个对象的创建的字节码干脆给你替换掉,也就是说C2编译器生成的机器码他已经可以把你原本的字节码改的面目全非,就干脆不会创建对象了(本例子),所以速度一下子提升了这么多。这种优化手段叫逃逸分析。

    如果在上面代码中关掉逃逸分析(-XX:-DoEscapeAnalysis),那么加上这个虚拟机参数后运行的话,可以看到这200次循环的时间后面虽然比前面也少了点,但不会出现明显的变成3位数等现象,这说明Object对象仍然是被创建。即关闭逃逸分析的话,他就没办法进入最终的C2编译器阶段了。

    方法内联

    方法内联也是即时编译器的优化手段的一种,比如下面的例子:

    private static int square(final int i) {
       
    	return i * i;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    该例子是求平方的函数,内部针对整数i做了平方。其实,我们去调用这个函数时:

    System.out.println(square(9));
    
    • 1

    比如,他认为这个函数如果比较短,而且这种函数如果是热点代码时,那么他就会做一个叫“方法内联”。方法内联非常简单,他就是把函数内的代码拷贝到他的调用者的位置,即进行方法内联以后变成下面的样子:

    System.out.println(9 * 9);
    
    • 1

    就是相当于把方法内的代码给他拿出来,放在方法调用者这里。并且他还能再做一个进一步的优化,因为每次调用的这个值(9)固定不变,就可以认为他是个常量,所以还可以做一个叫“常量折叠(constant folding)”的优化,直接把他算出来的结果就传给System.out.println:

    System.out.println(81);
    
    • 1

    以上是方法内联的优化策略。下面通过实验去观察方法内联的效率上的提升。

    public class Test {
       
    	// -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining -XX:CompileCommand=dontinline,*JIT2.square
    	// -XX:+PrintCompilation
    	public static void main(String[] args) {
       
    		int x = 0;
    		for (int i = 0; i < 500; i ++) {
       
    			long start = System.nanoTime();
    			for (int j = 0; j < 1000; j ++) {
       
    				x = square(9);
    			}
    			long end = System.nanoTime();
    			System.out.printf(
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
  • 相关阅读:
    【深度学习21天学习挑战赛】备忘篇:模型复用——模型的保存与加载
    小厂一面
    【Python自然语言处理】规则分词中正向、反向、双向最大匹配法的讲解及实战(超详细 附源码)
    图像预处理技术与算法
    【SQL刷题】Day12----SQL汇总数据专项练习
    第十五章:L2JMobius学习 – 刷新NPC和对话
    责任链模式与spring容器的搭配应用
    Springboot集成SLF4J+Logback
    P1807 最长路题解【Floyd】
    internship:熟悉项目代码的几个步骤
  • 原文地址:https://blog.csdn.net/PurineKing/article/details/127816965