從位元組碼視角看java字串的拼接

Chitty_Tina發表於2016-06-23

搞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:此兩個欄位,組合起來標識變數的使用範圍,如019,則標識這個變數在從第一個指令開始到最後一直有效(因為s1,s2是入參,),可以嘗試在程式碼中間定義個區域性變數,就可以看到start和length的不同。
slot:JVM規範裡面對區域性變數裡儲存一個區域性變數的儲存單元的叫法,是324個位元組,可以嘗試定義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



相關文章