從位元組碼視角看java字串的拼接
搞java的都知道,string直接用+拼接的時候,javac編譯會進行優化,因此字串拼接也推薦使用stringbuffer或者stringbuilder。那到底是怎麼優化的呢?簡單的程式碼如下
package test;
public class Java {
public String test(String s1, String s2) {
return s1 + s2;
}
public String test1(String s1, String s2) {
return new StringBuilder(s1).append(s2).toString();
}
}
服務端
服務端我們知道jvm是基於棧實現的,但編譯的時候具體怎麼做優化了呢,javap工具閃亮登場
+拼接
public java.lang.String test(java.lang.String, java.lang.String);
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=3
0: new #16 // class java/lang/StringBuilder
3: dup
4: aload_1
5: invokestatic #18 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
8: invokespecial #24 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
11: aload_2
12: invokevirtual #27 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: invokevirtual #31 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
18: areturn
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 19 0 this Ltest/Java;
0 19 1 s1 Ljava/lang/String;
0 19 2 s2 Ljava/lang/String;
簡單說明一下:
flags:標識方法的訪問許可權,此方法是public的
code:包括完整的編譯後指令、區域性變數等資訊
stack:棧大小,服務端jvm是基於棧實現的,後面再分析為什麼為3
locals:區域性變數佔用大小,後面分析為什麼為3
LineNumberTable:對應原始碼行,這個debug的時候很有用了
LocalVariableTable:區域性變數表
start,length:此兩個欄位,組合起來標識變數的使用範圍,如0,19,則標識這個變數在從第一個指令開始到最後一直有效(因為s1,s2是入參,),可以嘗試在程式碼中間定義個區域性變數,就可以看到start和length的不同。
slot:JVM規範裡面對區域性變數裡儲存一個區域性變數的儲存單元的叫法,是32位4個位元組,可以嘗試定義long,就可以發現會佔用2個slot。這裡三個都是ref引用,因此locals為3,即三個slot。
中間一大塊,明顯可以看到,string用+拼接,已經被優化為使用stringbuilder了,具體的指令含義,可參考
http://en.wikipedia.org/wiki/Java_bytecode_instruction_listings
比如
aload_1,就是把區域性變數裡面的s1,壓入到棧當中。jvm提供了aload_0到aload_3共類似4個指令,那如果區域性變數超過4個怎麼辦呢,當然還有aload指令,直接指定區域性變數就好了。
我們知道服務端的jvm是基於棧實現的,上面的aload就是把區域性變數壓入棧當中,因此在編譯後已經計算好棧的長度,也就是
stack大小,上述例子中,stringBuilder,s1,s2三個區域性變數都會壓入棧,因此運算元棧3個就足夠了,那如果是兩個int相加(int a=1+2)返回呢?是否也是三個呢,答案是2,如果是兩個long相加又是多少呢?(long要佔用8個位元組)
看兩個字串的直接+拼接方法,已經優化為採用stringbuilder進行了拼裝。
stringbuilder拼接
public java.lang.String test1(java.lang.String, java.lang.String);
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=3
0: new #16 // class java/lang/StringBuilder
3: dup
4: aload_1
5: invokespecial #24 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
8: aload_2
9: invokevirtual #27 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
12: invokevirtual #31 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
15: areturn
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this Ltest/Java;
0 16 1 s1 Ljava/lang/String;
0 16 2 s2 Ljava/lang/String;
看結果,相比自動優化,減少了一個invokestatic指令的執行,編譯後的位元組碼佔用也少3個位元組。
android
客戶端android,jvm是基於暫存器的,那編譯後會是怎麼樣呢?dexdump工具閃亮登場
android在編譯時,是把.class檔案再轉換為dex檔案,所有的class都合併一起,這帶來節約空間,沒有服務端那樣每個class就一個很大的常量池,但其方法數是用short來計數的,這也帶來了64k問題,android也提供mutildex解決方法,跑題了,dexdump就是對class.dex進行解析。
+拼接
#0 : (in Lcom/sunqi/service/Java;)
name : 'test'
type : '(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;'
access : 0x0001 (PUBLIC)
code -
registers : 5
ins : 3
outs : 2
insns size : 18 16-bit code units
007578: |[007578] com.sunqi.service.Java.test:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
007588: 2200 6d00 |0000: new-instance v0, Ljava/lang/StringBuilder; // type@006d
00758c: 7110 3001 0300 |0002: invoke-static {v3}, Ljava/lang/String;.valueOf:(Ljava/lang/Object;)Ljava/lang/String; // method@0130
007592: 0c01 |0005: move-result-object v1
007594: 7020 3201 1000 |0006: invoke-direct {v0, v1}, Ljava/lang/StringBuilder;.<init>:(Ljava/lang/String;)V // method@0132
00759a: 6e20 3501 4000 |0009: invoke-virtual {v0, v4}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; // method@0135
0075a0: 0c00 |000c: move-result-object v0
0075a2: 6e10 3601 0000 |000d: invoke-virtual {v0}, Ljava/lang/StringBuilder;.toString:()Ljava/lang/String; // method@0136
0075a8: 0c00 |0010: move-result-object v0
0075aa: 1100 |0011: return-object v0
catches : (none)
positions :
0x0000 line=5
locals :
0x0000 - 0x0012 reg=2 this Lcom/sunqi/service/Java;
0x0000 - 0x0012 reg=3 s1 Ljava/lang/String;
0x0000 - 0x0012 reg=4 s2 Ljava/lang/String;
大體結構與之前有些類似,name標識方法名,type標識入參,access是訪問許可權,positions對應程式碼行,locals對應區域性變數,只是裡面使用暫存器。
registers:暫存器數,因為移動端主要是基於arm的cpu架構,RISC採用暫存器來實現,關於棧和暫存器哪種實現好,優缺點是什麼,本文不作討論,具體大家可以去google一下stack vs registers
程式碼編譯後的指令,與服務端則也有點類似,只是指令已經不同,具體請參考
https://source.android.com/devices/tech/dalvik/dalvik-bytecode.html
stringbuilder拼接
#1 : (in Lcom/sunqi/service/Java;)
name : 'test1'
type : '(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;'
access : 0x0001 (PUBLIC)
code -
registers : 4
ins : 3
outs : 2
insns size : 14 16-bit code units
0075ac: |[0075ac] com.sunqi.service.Java.test1:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
0075bc: 2200 6d00 |0000: new-instance v0, Ljava/lang/StringBuilder; // type@006d
0075c0: 7020 3201 2000 |0002: invoke-direct {v0, v2}, Ljava/lang/StringBuilder;.<init>:(Ljava/lang/String;)V // method@0132
0075c6: 6e20 3501 3000 |0005: invoke-virtual {v0, v3}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; // method@0135
0075cc: 0c00 |0008: move-result-object v0
0075ce: 6e10 3601 0000 |0009: invoke-virtual {v0}, Ljava/lang/StringBuilder;.toString:()Ljava/lang/String; // method@0136
0075d4: 0c00 |000c: move-result-object v0
0075d6: 1100 |000d: return-object v0
catches : (none)
positions :
0x0000 line=9
locals :
0x0000 - 0x000e reg=1 this Lcom/sunqi/service/Java;
0x0000 - 0x000e reg=2 s1 Ljava/lang/String;
0x0000 - 0x000e reg=3 s2 Ljava/lang/String;
對比直接+拼裝的位元組碼,可以看到直接用stringbuilder拼裝少執行兩個指令,同時也只要4個暫存器就夠了。
不過比較java服務端的位元組碼和android位元組碼,發現機遇暫存器的明顯使用更少的指令即可以實現功能。基於棧實現需要更多的指令和記憶體訪問,這也是移動端採用暫存器架構的原因之一吧
總結:
1、我們需要了解,底層的實現到底是怎麼樣的,不同的平臺有不同特點
2、有時候需要從另一個視角看我們寫的程式碼,在機器上執行會是怎麼樣,以便於更好的寫程式碼
感謝 分享 http://sunqi.iteye.com/blog/2273373
【騰訊內部乾貨分享】分析Dalvik位元組碼進行減包優化 http://wetest.qq.com/lab/view/?id=96&from=ads_test2_qqtips&sessionUserType=BFT.PARAMS.193009.TASKID&ADUIN=532007154&ADSESSION=1466646757&ADTAG=CLIENT.QQ.5467_.0&ADPUBNO=26558
相關文章
- 從 Java 位元組碼到 ASM 實踐JavaASM
- Java程式碼中字串拼接方式分析Java字串
- Java 位元組碼Java
- 從位元組碼看java型別轉換【 深入理解 (T[]) new Object[size] 】Java型別Object
- Java程式碼如何檢視位元組碼及彙編碼Java
- 從1+1=2來理解Java位元組碼Java
- JAVA動態位元組碼Java
- 【Java】JVM位元組碼分析JavaJVM
- 從全域性視角看資料結構資料結構
- Dalvik 和 Java 位元組碼的比較Java
- 輕鬆看懂Java位元組碼Java
- Java位元組碼指令表Java
- Java類轉位元組碼工具Java
- Java 字串比較、拼接問題Java字串
- Java字串拼接寫法 joiner.onJava字串
- 上帝視角看 TypeScriptTypeScript
- 例項分析理解Java位元組碼Java
- Java位元組碼增強技術Java
- Java 8中字串拼接新姿勢:StringJoinerJava字串
- Java中常見字串拼接九種方式Java字串
- Java的位元組碼和ABAP load的比較Java
- Java併發雜談(一):volatile的底層原理,從位元組碼到CPUJava
- 從服務端視角看高併發難題服務端
- 位元組碼
- 使用javap分析Java位元組碼的一個例子Java
- JavaScript 字串拼接JavaScript字串
- Java 動態性(4) – 位元組碼操作Java
- 學習 Java 之 位元組碼驗證Java
- JVM視角看物件建立JVM物件
- 訂單視角看支付
- 開啟java語言世界通往位元組碼世界的大門——ASM位元組碼操作類庫JavaASM
- ASM位元組碼操作類庫:開啟java語言世界通往位元組碼世界的大門ASMJava
- ASM位元組碼操作類庫(開啟java語言世界通往位元組碼世界的大門)ASMJava
- 前端視角看視訊處理前端
- 從前端程式設計師的視角看小程式的穩定性保障前端程式設計師
- PyCon 2018: 中文視訊(1):理解位元組碼
- 起量是玄學嗎?——從上帝視角看買量
- 從NLP視角看電視劇《狂飆》,會有什麼發現?
- JavaScript計算字串位元組長度JavaScript字串