1. 程式人生 > >JVM字節碼與Java代碼層調優

JVM字節碼與Java代碼層調優

構造 了解 arraycopy {} 修飾符 表達 stack 資源 cmd

jvm字節碼指令

我們都知道,Java源代碼不會像C/C++那樣直接被編譯為機器碼,而是被編譯成字節碼,這造就了Java可以跨平臺的特性。JVM實際執行的也是編譯後的字節碼,所以想要在Java代碼層進行調優,就得對字節碼有一定的了解。

.class文件是無法直接使用文本編輯器查看的,至於字節碼的查看,我們可以使用javap這個jdk自帶的工具。javap是 Java class文件分解器,可以反編譯(即對javac編譯的文件進行反編譯),也可以查看java編譯器生成的字節碼,用於分解class文件。用法如下:
技術分享圖片

我們先來寫一個簡單的測試類代碼:

public class Test1 {

    public static void main(String[] args) {
        int a = 2;
        int b = 3;
        int c = a + b;
        System.out.println(c);
    }
}

然後讓我們來看看這些源代碼編譯後的字節碼是長什麽樣的,進入該類的.class文件存放路徑,打開cmd命令行,執行如下命令,將字節碼重定向到一個.txt文件中:

javap -verbose Test1.class > Test1.txt

打開Test1.txt文件,該文件內容如下:

Classfile /D:/Java_Work/classesview/out/production/classesview/classesview/Test1.class // class文件的路徑
  Last modified 2018年7月27日; size 573 bytes  // 最後一次修改時間以及該class文件的大小
  MD5 checksum 6ccc47493e2c660409ad2f057996f117  // 該類的MD5值
  Compiled from "Test1.java"  // 源碼文件名
public class classesview.Test1  // 包名及類名
  minor version: 0  // 版本號
  major version: 52 // 版本號
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER  // 該類的權限修飾符
  this_class: #4                          // classesview/Test1  // 當前類,即this的指向
  super_class: #5                         // java/lang/Object   // 父類,即super的指向
  interfaces: 0, fields: 0, methods: 2, attributes: 1 // 接口數量、字段數量、方法數量、屬性數量
Constant pool:  // 常量池,這些有順序的數字相當於是常量池裏的一個索引
   #1 = Methodref          #5.#23         // java/lang/Object."<init>":()V  // 方法引用(符號引用)
   #2 = Fieldref           #24.#25        // java/lang/System.out:Ljava/io/PrintStream; // 字段引用
   #3 = Methodref          #26.#27        // java/io/PrintStream.println:(I)V
   #4 = Class              #28            // classesview/Test1  // 類引用
   #5 = Class              #29            // java/lang/Object
   #6 = Utf8               <init>  // 這是字節碼中構造函數的名稱,而名稱就是一段字符串,所以就會按照編碼標識
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Lclassesview/Test1;
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               args
  #16 = Utf8               [Ljava/lang/String;
  #17 = Utf8               a
  #18 = Utf8               I
  #19 = Utf8               b
  #20 = Utf8               c
  #21 = Utf8               SourceFile
  #22 = Utf8               Test1.java
  #23 = NameAndType        #6:#7          // "<init>":()V  // 返回值
  #24 = Class              #30            // java/lang/System
  #25 = NameAndType        #31:#32        // out:Ljava/io/PrintStream;
  #26 = Class              #33            // java/io/PrintStream
  #27 = NameAndType        #34:#35        // println:(I)V
  #28 = Utf8               classesview/Test1
  #29 = Utf8               java/lang/Object
  #30 = Utf8               java/lang/System
  #31 = Utf8               out
  #32 = Utf8               Ljava/io/PrintStream;
  #33 = Utf8               java/io/PrintStream
  #34 = Utf8               println
  #35 = Utf8               (I)V
{
  public classesview.Test1();  // 構造函數
    descriptor: ()V  // 方法描述符,這裏的V表示void
    flags: (0x0001) ACC_PUBLIC  // 權限修飾符
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lclassesview/Test1;

  public static void main(java.lang.String[]);  // main方法
    descriptor: ([Ljava/lang/String;)V  // 方法描述符,[表示引用了一個數組類型,L則表示引用的類後面跟的就是類名
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC  // 權限修飾符
    Code:
      // 操作數棧的深度2,當調用一個方法的時候,實際上在JVM裏對應的是一個棧幀入棧出棧的過程
      // 本地變量表最大長度(slot為單位),64位的是2,其他是1,索引從0開始,如果是非static方法索引0代表this,後面是入參,後面是本地變量
      // 1個參數,實例方法多一個this參數
      stack=2, locals=4, args_size=1
         0: iconst_2              // 常量2壓棧
         1: istore_1              // 出棧保存到本地變量1裏面 
         2: iconst_3              // 常量3壓棧
         3: istore_2              // 出棧保存到本地變量2裏面 
         4: iload_1               // 局部變量1壓棧 
         5: iload_2               // 局部變量2壓棧
         6: iadd                  // 棧頂兩個元素相加,計算結果壓棧
         7: istore_3              // 出棧保存到局部變量3裏面
         8: getstatic     #2      // 這裏對應的是常量池裏的#2
        11: iload_3               // 局部變量3壓棧
        12: invokevirtual #3      // 這裏對應的是常量池裏的#3
        15: return                // return void
      LineNumberTable:  // 行號表
        line 5: 0  // 源代碼的第5行,0代表字節碼裏的0,也就是上面的常量2壓棧那一行
        line 6: 2  // 源代碼的第6行,2代表字節碼裏的2,也就是上面的常量3壓棧那一行
        line 7: 4  // 以此類推...
        line 8: 8
        line 9: 15
      LocalVariableTable:  // 本地變量表
        Start  Length  Slot  Name   Signature
            0      16     0  args   [Ljava/lang/String; // 索引為0,變量名稱為args
            2      14     1     a   I                   // 索引為2,變量名稱為a
            4      12     2     b   I                   // 索引為4,變量名稱為b
            8       8     3     c   I                   // 索引為5,變量名稱為c
}
SourceFile: "Test1.java"  // 源碼文件名

字節碼裏的指令與源代碼的一個對應關系:
技術分享圖片

從以上的字節碼中,可以看到和Java的源代碼是不太一樣的,字節碼裏面還會用描述符來描述字段和方法,描述符有時候也被稱之為簽名(Signature),字段描述符與源代碼裏的字段:
技術分享圖片

方法描述符與源代碼裏的方法:
技術分享圖片

JVM在執行字節碼指令的時候,是基於棧的架構,與OS中基於寄存器的架構不太一樣。基於棧的好處就是指令比較短,但是指令集就會比較長了。以下用了幾張圖片來描述執行以上main方法裏的字節碼指令時,操作數棧裏的一個出棧入棧的過程:
技術分享圖片
技術分享圖片
技術分享圖片
技術分享圖片
技術分享圖片
技術分享圖片


i++ 與 ++i

我們經常會看到一個問題,i++ 與 ++i 哪個效率更高,在循環裏應該使用哪一個好。雖然很多人都知道答案,但是可能不知道答案背後的原理。所以本小節將介紹一下 i++ 與 ++i 效率孰高孰低的原理。首先也是準備一些簡單的測試代碼,如下:

public class SelfAdd {

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

    public static void f1() {
        for(int i=0;i<10;i++) {
            System.out.println(i);
        }
    }

    public static void f2() {
        for(int i=0;i<10;++i) {
            System.out.println(i);
        }
    }

    public static void f3() {
        int i=0;
        int j = i++;
        System.out.println(j);
    }

    public static void f4() {
        int i=0;
        int j = ++i;
        System.out.println(j);
    }
}

以上代碼編譯後的字節碼如下:

Classfile /D:/Java_Work/classesview/out/production/classesview/classesview/SelfAdd.class
  Last modified 2018年7月27日; size 980 bytes
  MD5 checksum f77d50197f39e7c67717f14297cbb504
  Compiled from "SelfAdd.java"
public class classesview.SelfAdd
  minor version: 0
  major version: 52
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #6                          // classesview/SelfAdd
  super_class: #7                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 6, attributes: 1
Constant pool:
   #1 = Methodref          #7.#29         // java/lang/Object."<init>":()V
   #2 = Methodref          #6.#30         // classesview/SelfAdd.f3:()V
   #3 = Methodref          #6.#31         // classesview/SelfAdd.f4:()V
   #4 = Fieldref           #32.#33        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #34.#35        // java/io/PrintStream.println:(I)V
   #6 = Class              #36            // classesview/SelfAdd
   #7 = Class              #37            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lclassesview/SelfAdd;
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               args
  #18 = Utf8               [Ljava/lang/String;
  #19 = Utf8               f1
  #20 = Utf8               i
  #21 = Utf8               I
  #22 = Utf8               StackMapTable
  #23 = Utf8               f2
  #24 = Utf8               f3
  #25 = Utf8               j
  #26 = Utf8               f4
  #27 = Utf8               SourceFile
  #28 = Utf8               SelfAdd.java
  #29 = NameAndType        #8:#9          // "<init>":()V
  #30 = NameAndType        #24:#9         // f3:()V
  #31 = NameAndType        #26:#9         // f4:()V
  #32 = Class              #38            // java/lang/System
  #33 = NameAndType        #39:#40        // out:Ljava/io/PrintStream;
  #34 = Class              #41            // java/io/PrintStream
  #35 = NameAndType        #42:#43        // println:(I)V
  #36 = Utf8               classesview/SelfAdd
  #37 = Utf8               java/lang/Object
  #38 = Utf8               java/lang/System
  #39 = Utf8               out
  #40 = Utf8               Ljava/io/PrintStream;
  #41 = Utf8               java/io/PrintStream
  #42 = Utf8               println
  #43 = Utf8               (I)V
{
  public classesview.SelfAdd();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lclassesview/SelfAdd;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: invokestatic  #2                  // Method f3:()V
         3: invokestatic  #3                  // Method f4:()V
         6: return
      LineNumberTable:
        line 6: 0
        line 7: 3
        line 8: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  args   [Ljava/lang/String;

  public static void f1();
    descriptor: ()V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=0
         0: iconst_0
         1: istore_0
         2: iload_0
         3: bipush        10
         5: if_icmpge     21
         8: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: iload_0
        12: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        15: iinc          0, 1
        18: goto          2
        21: return
      LineNumberTable:
        line 28: 0
        line 29: 8
        line 28: 15
        line 31: 21
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            2      19     0     i   I
      StackMapTable: number_of_entries = 2
        frame_type = 252 /* append */
          offset_delta = 2
          locals = [ int ]
        frame_type = 250 /* chop */
          offset_delta = 18

  public static void f2();
    descriptor: ()V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=0
         0: iconst_0
         1: istore_0
         2: iload_0
         3: bipush        10
         5: if_icmpge     21
         8: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: iload_0
        12: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        15: iinc          0, 1
        18: goto          2
        21: return
      LineNumberTable:
        line 51: 0
        line 52: 8
        line 51: 15
        line 54: 21
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            2      19     0     i   I
      StackMapTable: number_of_entries = 2
        frame_type = 252 /* append */
          offset_delta = 2
          locals = [ int ]
        frame_type = 250 /* chop */
          offset_delta = 18

  public static void f3();
    descriptor: ()V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
         0: iconst_0
         1: istore_0
         2: iload_0
         3: iinc          0, 1
         6: istore_1
         7: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: iload_1
        11: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        14: return
      LineNumberTable:
        line 72: 0
        line 73: 2
        line 74: 7
        line 75: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            2      13     0     i   I
            7       8     1     j   I

  public static void f4();
    descriptor: ()V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
         0: iconst_0
         1: istore_0
         2: iinc          0, 1
         5: iload_0
         6: istore_1
         7: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: iload_1
        11: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        14: return
      LineNumberTable:
        line 93: 0
        line 94: 2
        line 95: 7
        line 96: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            2      13     0     i   I
            7       8     1     j   I
}
SourceFile: "SelfAdd.java"

我們先來看f4();和f3();,也就是i++與++i本身的字節碼,這裏沒有涉及循環,兩者的字節碼與源代碼的對比如下:

f4();                        
int i=0;                     
int j = ++i;                 

0: iconst_0        // 常量0壓棧                
1: istore_0        // 出棧保存到本地變量0裏面,即代碼中的變量i
2: iinc      0, 1  // 本地變量0加1            
5: iload_0         // 本地變量0壓棧,此時這個本地變量的值為1
6: istore_1        // 出棧保存到本地變量1裏面,即代碼中的變量j

f3();
int i=0;
int j = i++;

0: iconst_0         // 常量0壓棧
1: istore_0         // 出棧保存到本地變量0裏面,即代碼中的變量i
2: iload_0          // 本地變量0壓棧
3: iinc       0, 1  // 本地變量0加1,註意:這裏是本地變量加1,不是操作數棧,棧裏依舊是0
6: istore_1         // 出棧保存到本地變量1裏面,即代碼中的變量j

從字節碼層面上,可以看到兩者之間始終是區別於先+還是後+,並沒有哪裏少操作或多操作了一步。知道了 i++和++i 在字節碼中的執行原理後,我們再來看看f1();和f2();方法裏這種使用了循環的字節碼,如下:

  public static void f1();
    descriptor: ()V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=0
         0: iconst_0             // 常量0壓棧
         1: istore_0             // 出棧保存到本地變量0裏面
         2: iload_0              // 從本地變量0壓棧
         3: bipush        10     // 常量10壓棧,因為取值為-128~127,所以采用bipush指令
         5: if_icmpge     21     // 若判斷的結果是大於就跳轉到第21行
         8: getstatic     #4     // 對應常量池的#4             
        11: iload_0              // 本地變量0壓棧
        12: invokevirtual #5     // 對應常量池的#5             
        15: iinc          0, 1   // 本地變量0加1
        18: goto          2      // goto到第2行
        21: return               // return void

  public static void f2();
    descriptor: ()V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=0
         0: iconst_0
         1: istore_0
         2: iload_0
         3: bipush        10
         5: if_icmpge     21
         8: getstatic     #4                  
        11: iload_0
        12: invokevirtual #5                  
        15: iinc          0, 1
        18: goto          2
        21: return

可以看到,這兩個方法的字節碼是一樣的。所以實際上循環裏無論是用++i還是i++效率都是一樣的,當再有人問你這個問題的時候,就可以理直氣壯的說它倆的效率是一樣的了。因為在字節碼裏,它倆的指令就是一模一樣的,沒有任何區別 。


字符串拼接

我們都知道,在循環裏拼接字符串的話,要使用StringBuilder或StringBuffer。如果直接使用 + 進行字符串拼接的話,效率就會很低。那麽為什麽使用 + 進行字符串拼接效率就低,而使用StringBuilder或StringBuffer進行字符串拼接效率就高呢?這就是本小節將要說明的一個問題。同樣的,我們也是從字節碼的角度來看,首先編寫一些測試代碼 ,如下:

public class StringAdd {
    public static void main(String[] args) {
        f1();
        f2();
    }

    public static void f1() {
        String src = "";
        for (int i = 0; i < 10; i++) {
            //每一次循環都會new一個StringBuilder
            src = src + "A";
        }
        System.out.println(src);
    }

    public static void f2() {
        //只要一個StringBuilder
        StringBuilder src = new StringBuilder();
        for (int i = 0; i < 10; i++) {
            src.append("A");
        }
        System.out.println(src);
    }
}

以上代碼編譯後的字節碼如下,我這裏只截取了f1();f2();方法的部分字節碼:

  public static void f1();
    descriptor: ()V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
         0: ldc           #4                  // 將常量字符串壓棧,對應常量池中的#4 
         2: astore_0                          // 出棧保存到本地變量0裏面
         3: iconst_0                          // 常量0壓棧
         4: istore_1                          // 出棧保存到本地變量1裏面
         5: iload_1                           // 本地變量1壓棧
         6: bipush        10                  // 常量10壓棧,因為取值為-128~127,所以采用bipush指令
         8: if_icmpge     37                  // 若判斷結果為大於,則執行第37行
        11: new           #5                  // class java/lang/StringBuilder 創建StringBuilder實例,如果是jdk1.4之前的版本,這裏創建的是StringBuffer
        14: dup                               // 復制StringBuilder實例的引用並壓入棧頂
        15: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V  使用空構造器創建的
        18: aload_0                           // 本地變量0壓棧,aload是用於將對象引用壓棧的指令
        19: invokevirtual #7                  // Method java/lang/StringBuilder.append: (Ljava/lang/String;)Ljava/lang/StringBuilder; 執行了append方法
        22: ldc           #8                  // 字符串A壓棧,取值-2147483648~2147483647采用ldc指令
        24: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;  執行了append方法
        27: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;  執行了toString方法
        30: astore_0                          // 將對象引用出棧保存到本地變量0裏面
        31: iinc          1, 1                // 本地變量1加1
        34: goto          5                   // 跳轉到第5行
        37: getstatic     #10                 // Field java/lang/System.out:Ljava/io/PrintStream;
        40: aload_0                           // 本地變量0壓棧,aload是用於將對象引用壓棧的指令
        41: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        44: return                            // return void

  public static void f2();
    descriptor: ()V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
         0: new           #5              // class java/lang/StringBuilder  創建StringBuilder實例
         3: dup                           // 復制StringBuilder實例的引用並壓入棧頂
         4: invokespecial #6              // Method java/lang/StringBuilder."<init>":()V
         7: astore_0                      // 將對象引用出棧並保存到本地變量0裏面
         8: iconst_0                      // 常量0壓棧
         9: istore_1                      // 出棧保存到本地變量1裏面
        10: iload_1                       // 本地變量1壓棧
        11: bipush        10              // 常量10壓棧,因為取值為-128~127,所以采用bipush指令
        13: if_icmpge     29              // 若判斷結果為大於,則執行第29行
        16: aload_0                       // 本地變量0壓棧,aload是用於將對象引用壓棧的指令
        17: ldc           #8              // 字符串A壓棧,取值-2147483648~2147483647采用ldc指令
        19: invokevirtual #7              // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;  執行了append方法
        22: pop
        23: iinc          1, 1            // 本地變量1加1
        26: goto          10              // 跳轉到第10行
        29: getstatic     #10             // Field java/lang/System.out:Ljava/io/PrintStream;
        32: aload_0                       // 本地變量0壓棧,aload是用於將對象引用壓棧的指令
        33: invokevirtual #12             // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        36: return                        // return void

從以上f1();方法的字節碼中,可以看到,在循環中使用 + 進行字符串拼接的話,每次循環都會new一個StringBuilder實例,同樣的也是需要執行append方法來拼接字符串,最後還需要執行toString方法轉換成字符串類型。而f2();方法的字節碼中,只創建了一次StringBuilder的實例,並且執行的指令也要少一些。所以使用StringBuilder進行字符串拼接,比使用 + 拼接的效率高。


Try-Finally字節碼

除了以上小節所提到的問題外,還有一個問題也很常見,這是一個關於Try-Finally的題目。就是try裏會return一個字符串,而finally裏則會改變這個字符串。那麽到底會返回改變前的字符串還是改變後的字符串。代碼如下:

public class TryFinally {
    public static void main(String[] args) {
        System.out.println(f1());
    }

    public static String f1() {
        String str = "hello";
        try{
            return str;
        } finally {
            str = "finally";
        }
    }
}

這個問題,我們同樣可以從字節碼的層面進行分析。將以上代碼編譯後的字節碼如下,我這裏只截取了f1();方法的部分字節碼,免得一些已經介紹過的內容占用篇幅:

  public static java.lang.String f1();
    descriptor: ()Ljava/lang/String;
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=0
         0: ldc           #5             // String hello 壓棧
         2: astore_0                     // 出棧保存到本地變量0裏
         3: aload_0                      // 本地變量0壓棧
         4: astore_1                     // 出棧保存到本地變量1裏
         5: ldc           #6             // String finally 壓棧
         7: astore_0                     // 出棧保存到本地變量0裏
         8: aload_1                      // 本地變量1壓棧
         9: areturn                      // 返回棧頂元素
        10: astore_2                     // 如果發生異常,操作棧裏就會存在一個異常對象,此時就會把異常對象出棧保存到本地變量2裏,然後執行以下指令
        11: ldc           #6             // String finally 壓棧
        13: astore_0                     // 出棧保存到本地變量0裏
        14: aload_2                      // 本地變量2壓棧
        15: athrow                       // 拋出異常,即把本地變量2裏存儲的異常對象給拋出

如上,從字節碼中進行分析,我們就可以很清晰的看到f1();方法到底會返回哪一個字符串。所以我們才要學會分析字節碼,這樣我們就能夠看到代碼執行的本質,而不是去死記硬背這些怪題,下次再遇到這種類似代碼就不會一臉懵逼了。


String Constant Variable

在關於字符串拼接那一小節中,我們得知了在使用 + 進行字符串拼接的時候,實際上會創建StringBuilder實例來完成字符串的拼接。但使用 + 進行字符串拼接,背後就一定是StringBuilder嗎?實際上是未必的,這取決於是常量拼接還是變量拼接。同樣的,我們來編寫一些測試代碼,然後從字節碼的層面上去觀察。代碼如下:

public class Constant {

    public static void main(String[] args) {
        f1();
        new Constant().f2();
    }

    public static void f1() {
        final String x = "hello";
        final String y = x + "world";
        String z = x + y;
        System.out.println(z);
    }

    public void f2() {
        final String x = "hello";
        String y = x + "world";
        String z = x + y;
        System.out.println(z);
    }
}

將以上代碼編譯後的字節碼如下,我這裏只截取了f1();f2();方法的部分字節碼:

  public static void f1();
    descriptor: ()V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=0
         0: ldc           #6                  // 字符串hello壓棧
         2: astore_0                          // 出棧保存到本地變量0裏面
         3: ldc           #7                  // 字符串helloworld壓棧
         5: astore_1                          // 出棧保存到本地變量1裏面
         6: ldc           #8                  // 字符串hellohelloworld壓棧
         8: astore_2                          // 出棧保存到本地變量2裏面
         9: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        12: aload_2                           // 本地變量2壓棧
        13: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V  此時操作棧裏只有本地變量2,所以打印本地變量2
        16: return                            // retrun void

  public void f2();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: ldc           #6                  // 字符串hello壓棧
         2: astore_1                          // 出棧保存到本地變量1裏面
         3: ldc           #7                  // 字符串helloworld壓棧
         5: astore_2                          // 出棧保存到本地變量2裏面
         6: new           #11                 // class java/lang/StringBuilder 創建StringBuilder實例,因為到這一步就是變量進行拼接了
         9: dup                               // 復制StringBuilder實例的引用並壓入棧頂
        10: invokespecial #12                 // Method java/lang/StringBuilder."<init>":()V  調用構造函數完成實例的構造
        13: ldc           #6                  // 字符串hello壓棧
        15: invokevirtual #13                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;  調用append方法拼接字符串
        18: aload_2                           // 本地變量2壓棧
        19: invokevirtual #13                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;  調用append方法拼接字符串
        22: invokevirtual #14                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;  調用toString方法轉換為字符串類型
        25: astore_3                          // 出棧保存到本地變量3裏面
        26: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        29: aload_3                           // 本地變量3壓棧
        30: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V  此時操作棧裏只有本地變量3,所以打印本地變量3
        33: return                            // return void

從以上的字節碼中,可以看到,當常量字符串進行拼接的時候,並沒有使用SpringBuilder去完成拼接,而是直接使用了一個新的字符串進行賦值,這其實是JVM在編譯時會對這種常量及字面常量進行替換,因為字節碼裏面是沒有 + 的概念的。所以只有變量拼接才會使用SpringBuilder去完成拼接。


常用代碼優化方法

常用代碼優化方法:

  • 盡量復用對象,不要循環創建對象,比如:for循環的字符串拼接
  • 容器類初始化的時候最好是指定長度,例如List、Map等,可以減少動態擴容的次數
  • ArrayList隨機遍歷快,LinkedList添加刪除快
  • 集合遍歷時,盡量減少重復計算
  • 盡量使用Entry來遍歷Map,代碼示例(1)
  • 大數組復制使用System.arraycopy,因為該方法底層是使用C實現的,所以效率高些
  • 局部變量盡量使用基本類型,而不是包裝類型
  • 不要手動調用System.gc(),因為GC是會耗時
  • 及時消除過期對象的引用,防止內存泄露
  • 盡量使用局部變量,減小變量的作用域
  • 盡量使用非同步的容器,例如ArrayList和Vector中,應該選用ArrayList,因為Vector裏大量使用了synchronized,會導致效率低下
  • 盡量減小同步作用範圍,例如synchronized方法和synchronized代碼塊中,應該選用synchronized代碼塊的方式完成同步
  • 可以使用ThreadLocal緩存線程不安全的對象或重量級的對象,例如SimpleDateFormat
  • 盡量使用延遲加載,例如在單例模式中就不要使用懶漢式的,代碼示例(2)
  • 盡量減少使用反射,如果是必須使用反射,則把反射出來的對象加緩存裏,這樣能避免使用反射的次數
  • 盡量使用連接池、線程池、對象池、緩存
  • 及時釋放資源,I/O流、Socket、數據庫連接對象
  • 慎用異常,不要用拋異常來表示正常的業務邏輯
  • String操作盡量少用支持正則表達式的方法,因為支持正則表達式方法的性能比較低
  • 日誌輸出註意使用不同的日誌級別,避免導致日誌不停的輸出
  • 日誌中參數拼接使用占位符,代碼示例(3)

(1):

for (Map.Entry<String, String> entry : map.entrySet()) {
    String key = entry.getKey();
    String value = entry.getValue();
}

(2):

package classesview;

public class Singleton {

    private Singleton() {
    }

    private static class SingletonHolder{
        private static Singleton singleton = new Singleton();
    }

    public static Singleton getInstance(){
        return SingletonHolder.singleton;
    }
}

(3):

log.info("orderId" + orderId);  // 不推薦 
log.info("orderId:{}", orderId);  // 推薦

參考文檔

java虛擬機規範

  • https://docs.oracle.com/javase/specs/jvms/se8/html/index.html

java語言規範

  • https://docs.oracle.com/javase/specs/jls/se8/html/index.html

javap:

  • https://docs.oracle.com/javase/8/docs/technotes/tools/unix/javap.html

字段描述符

  • https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2

方法描述符

  • https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.3

字節碼指令:

  • https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html

常量池:

  • https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4

本地變量表:

  • https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6.1
  • https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.13

操作數棧:

  • https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6.2

Code屬性:

  • https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.3

LineNumberTable:

  • https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.12

constant variable:

  • https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.12.4

常量表達式

  • https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.28

String.intern

  • https://blog.csdn.net/goldenfish1919/article/details/80410349

String去重

  • https://blog.csdn.net/goldenfish1919/article/details/20233263

JVM字節碼與Java代碼層調優