JVM优化

指令优化

在谈到优化之前我们先看一个简单例子,非常简单的例子,查看编译后的文件的的指令是什么样子的,一个非常简单的java程序,Hello.java

1
2
3
4
5
6
7
8
public class Hello {
      public String getName() {
          return "a" ;
     }
     public static void main(String []args) {
         new Hello().getName();
     }
}

我们看看这段代码编译后指令会形成什么样子:

C:\>javac Hello.java

C:\>javap -verbose -private Hello
Compiled from “Hello.java”
public class Hello extends java.lang.Object
SourceFile: “Hello.java”
minor version: 0
major version: 50
Constant pool:
const #1 = Method       #6.#17; //  java/lang/Object.”<init>”:()V
const #2 = String       #18;    //  a
const #3 = class        #19;    //  Hello
const #4 = Method       #3.#17; //  Hello.”<init>”:()V
const #5 = Method       #3.#20; //  Hello.getName:()Ljava/lang/Stri
const #6 = class        #21;    //  java/lang/Object
const #7 = Asciz        <init>;
const #8 = Asciz        ()V;
const #9 = Asciz        Code;
const #10 = Asciz       LineNumberTable;
const #11 = Asciz       getName;
const #12 = Asciz       ()Ljava/lang/String;;
const #13 = Asciz       main;
const #14 = Asciz       ([Ljava/lang/String;)V;
const #15 = Asciz       SourceFile;
const #16 = Asciz       Hello.java;
const #17 = NameAndType #7:#8;//  "<init>":()V
const #18 = Asciz       a;
const #19 = Asciz       Hello;
const #20 = NameAndType #11:#12;//  getName:()Ljava/lang/String;
const #21 = Asciz       java/lang/Object;

{
public Hello();
Code:
Stack=1, Locals=1, Args_size=1
0:   aload_0
1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
4:   return
LineNumberTable:
line 11: 0

public java.lang.String getName();
Code:
Stack=1, Locals=1, Args_size=1
0:   ldc     #2; //String a
2:   areturn
LineNumberTable:
line 14: 0

public static void main(java.lang.String[]);
Code:
Stack=2, Locals=1, Args_size=1
0:   new     #3; //class Hello
3:   dup
4:   invokespecial   #4; //Method “<init>”:()V
7:   invokevirtual   #5; //Method getName:()Ljava/lang/String;
10:  pop
11:  return
LineNumberTable:
line 26: 0
line 30: 11

}

看起来乱七八糟,不要着急,这是一个最简单的java程序,我们按照正常的程序思路从main方法开始看,首先第一行是告诉你new #3;//class Hello,这个地方相当于执行了new Hello()这个命令,而#3是什么意思呢,在前面编译的指令列表中,找到对应的#3的位置,这就是我们所谓的入口位置,如果指令还要去寻找下一个指令就跟着#找到就可以了,就想刚才#3又找到#19,其实是要找到Hello的定义,也就是要引用到Class的定义的位置。

继续看下一步(关于内部入栈出栈的指令我们这里不多说明),invokespecial   #4; //Method “<init>”:()V,这个貌似看不太懂,不过可以看到后面是一个init方法,它到底初始化了什么,我们这里因为只有一行代码,我们姑且相信它初始化了Hello,不过invokespecial不是对super进行调用的时候才用到的吗?所以这里需要补充一下的就是当对象的初始化的时候,也会调用它,这里的初始化方法就是构造方法了,在指令的时候统一命名为init的说法;

那么调用它的构造方法,如果没有构造方法,肯定会进入Hello的默认构造方法,我们看看上面的public Hello(),发现它内部就执行了一条指令就是调用又调用一个invokespecial指令,这个指令其实就是初始化Object父对象的。

再继续看下一条指令:invokevirtual   #5; //Method getName:()Ljava/lang/String;你会发现是调用了getName的方法,采用的就是我们原先说的invokevirtual的指令,那么根据到getName方法部分去:

会发现直接做了一个ldc     #2; //String a操作就返回了,获取到对应的数据的地址后就直接返回了,执行的指令在位置#2,也就是在常量池中的一个2。

好了一个简单的程序指令就分析到这里了,更多的指令大家可以自己去分析,你就可以看明白java在指令上是如何处理的了,甚至于可以看出java在继承、内部类、静态内部类的包含关系是如何实现的了,它并不是没用,当你想成为一个更为专业和优秀的程序员,你应该知道这些,才能让你对这门驾驭得更加自如。

几个简单的测试下来,会发现一些常见的东西,比如

==>你继承一个类,那个类里面有一个public方法,在编译后,你会发现这个父亲类的方法的指令部分会被拷贝到子类中的最后面来

==>而当使用String做 “+” 的时候,那怕是多个 “+” ,JVM会自动编译指令时编译为StringBuilder的append的操作(JDK 1.5以前是StringBuffer),大家都知道append的操作将比 + 操作快非常的倍数,既然JVM做了这个指令转换,那么为什么还这么慢呢,当你发现java代码中的每一行做完这种+操作的时候,StringBuilder将会做一个toString()操作,如果下一次再运行就要申请一个新的StringBuilder,它的空间浪费在于toString和反复的空间申请;并且我们在前面探讨过,在默认情况下这个空间数组的大小是10,当超过这个大小时,将会申请一个双倍的空间来存放,并进行一次数组内容的拷贝,此时又存在一个内部空间转换的问题,就导致更多的问题,所以在单个String的加法操作中而且字符串不是太长的情况下,使用+是没有问题的,性能上也无所谓;当你采用很多循环、或者多条语句中字符串进行加法操作时,你就要注意了,比如读取文件这类;比如采用String a = “dd” + “bb” + “aa”;它在运行时的效率将会等价于StringBuilder buf = new StringBuilder().append(“dd”).append(“bb”).append(“aa”);

但是当发生以下情况的时候就不等价了(也就是不要在所有情况下寄希望于JVM为你优化所有的代码,因为代码具有很多不确定因素,JVM只是去优化一些常见的情况):

1、字符串总和长度超过默认10个字符长度(一般不是太长也看不出区别,因为本身也不慢)。

2、多次调用如上面的语句修改为String a = “dd”;a += “bb”; a += “aa”;与上面的那条语句的执行效率和空间开销都是完全不一样的,尤其是很多的时候。

3、循环,其实循环的基础就是来源于第二点的多次调用加法,当循环时肯定是多次调用这条语句;因为Java不知道你下一条语句要做什么,所以加法操作,它不得不将它toString返回给你。

==>继续测试你会发现内部类、静态内部类的一些特征,其实是将他编辑成为一个外部的class文件,用了一些$标志符号来分隔,并且你会发现内部类编译后的指令会将外包类的内容包含进来,只是他们通过一些标志符号来标志出它是内部类,它是那个类的内部类,而它是静态的还是静态的特征,用以在运行时如何来完成调用。

==>另外通过一些测试你还会发现java在编译时就优化的一个动作,当你的常量在编译时可能会在一些判定语句中直接被解析完成,比如一个boolean类型常量IS_PROD_SYS(表示是否为生产环境),如果这个常量如果是false,在一段代码中如果出现了代码片段:

1
2
3
if (IS_PROD_SYS) {
    .....
}

此时JVM编译器在运行时将会直接放弃这段代码,认为这段代码是没有意义的;反之,当你的值为true的时候,编译器会认为这个判定语句是无效的,编译后的代码,将会直接抛弃掉if语句,而直接运行内部的代码;这个大家在编译后的class文件通过反编译工具也可以看得出来的;其实java在运行时还做了很多的动作,下面再说说一些简单的优化,不过很多细节还是需要在工作中去发现,或者参考一些JVM规范的说明来完善知识。

上面虽然说明了很多测试结果所表明的JVM所为程序所做的优化,但是实际的情况却远远不止如此,本文也无法完全诠释JVM的真谛,而只是一个开头,其余的希望各位自己可以做相应的测试操作;

说完了一些常见的指令如何查看,以及通过查看指令得到一些结论,我们现在来看下指令在调用时候一般的优化方法一般有哪些(这里主要是在跨方法调用上,大家都知道,java方法建议很小,而且来回层次调用非常多,但是java依然推荐这样写,由上面的分析不得不说明的是,这样写了后,java来回调用会经过非常的class寻址以及在class对对内部的方法名称进行符号查表操作,虽然Hash算法可以让我们的查表提速非常的倍数,但是毕竟还是需要查表的,这些不变化的东西,我们不愿意让他反复的去做,因为作为底层程序,这样的开销是伤不起的,JVM也不会那么傻,我们来看看它到底做了什么):

==>在上面都看到,要去调用一个方法的call site,是非常麻烦的事情,虽然说static的是可以直接定位的,但是我们很多方法都不是,都是需要找到class的入口(虽然说Class的转换只需要一次,但是内部的方法调用并不是),然后查表定位,如果每个请求都是这样,就太麻烦了,我们想让内部的放入入口地址也只有一次,怎么弄呢?

==>在前面我们说了,JVM在加载后,一般不使用特殊的API,是不会造成Class的变化的,那么它在计算偏移量的时候,就可以在指令执行的过程中,将目标指令记忆,也就是在当前方法第一次翻译为指令时,在查找到目标方法的调用点后,我们希望在指令的后面记录下调用点的位置,下次多个请求调用到这个位置时,就不用再去寻找一次代码段了,而直接可以调用到目标地址的指令。

==>通过上面的优化我们貌似已经满足了自己的想法,不过很多时候我们愿意将性能进行到底,也就是在C++中有一种传说中的内联,inline,所以JVM在运行时优化中,如果发现目标方法的方法指令非常小的情况下,它会将目标方法的指令直接拷贝到自己的指令后面,而不需要再通过一次寻址时间,而是直接向下运行,所以JVM很多时候我们推荐使用小方法,这样对代码很清晰,对性能也不错,大的方法JVM是拒绝内联的(在C++中,这种内联需要自己去指定,而并非由系统完成,正常C++的指令也是按照入口+偏移量来找到的)

==>而对于继承关系的优化,通过层次模型的分析,我们在第四章中就已经说明,也就是利用一般情况下多态中的单个链中对应的对象的重写方法数组肯定不会太长,所以在Class的定义时我们就知道自下向上有多少个重写方法,而不是运行时才知道的,这个也叫做编译时的层次分析。

猜你喜欢

转载自flycw.iteye.com/blog/2396798