JVM字節碼與Java代碼層調優
我們都知道,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代碼層調優