JVM optimization

Instruction optimization

Before talking about optimization, let's look at a simple example, a very simple example, to see what the instructions of the compiled file look like, a very simple java program, 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();
     }
}

Let's see what the directive looks like when this code is compiled:

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

}

It looks messy, don't worry, this is the simplest java program, we start from the main method according to the normal program idea, the first line is to tell you new #3;//class Hello, this place is equivalent to executing The command new Hello(), and what does #3 mean? In the previously compiled instruction list, find the corresponding #3 position, which is what we call the entry position. If the instruction needs to find the next instruction, follow #Just find it, just think #3 and find #19 just now, in fact, to find the definition of Hello, that is, to refer to the location of the definition of Class.

Continue to see the next step (we won't explain much about the internal push and pop instructions here), invokespecial #4; //Method "<init>":()V, this seems to be incomprehensible, but you can see that the following is An init method, what exactly does it initialize? Because we only have one line of code here, we believe that it initializes Hello, but isn't invokespecial used when calling super? So what needs to be added here is when the object's When initializing, it will also be called. The initialization method here is the construction method, which is named as init in the instruction;

Then call its constructor. If there is no constructor, it will definitely enter the default constructor of Hello. Let's look at the public Hello() above and find that an instruction is executed inside it, which is to call and call an invokespecial instruction. This instruction is actually Is to initialize the Object parent object.

Continue to look at the next instruction: invokevirtual #5; //Method getName:()Ljava/lang/String; You will find that the method of getName is called, which is the instruction of invokevirtual we said earlier, then according to the method of getName Section goes to:

You will find that a ldc #2; //String a operation is returned, and the address of the corresponding data is obtained and returned directly. The executed instruction is in position #2, which is a 2 in the constant pool.

Well, a simple program instruction is analyzed here. For more instructions, you can analyze it yourself. You can see how java handles instructions, and you can even see java in inheritance, inner classes, How is the inclusion relationship of static inner classes implemented? It's not useless. When you want to become a more professional and excellent programmer, you should know these, so that you can control this more freely.

After a few simple tests, you will find some common things, such as

==> You inherit a class, and that class has a public method. After compiling, you will find that the instruction part of the method of the parent class will be copied to the end of the subclass.

==>而当使用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的定义时我们就知道自下向上有多少个重写方法,而不是运行时才知道的,这个也叫做编译时的层次分析。

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=326706847&siteId=291194637