《字串連線你用+還是用StringBuilder》續

超人汪小建發表於2018-07-16

前言

前面的一篇文章《字串連線你用+還是用StringBuilder》,有朋友找我反饋了一些問題,其中一位朋友說JDK10下生成的位元組碼跟文章中並不一樣,這裡繼續看下是什麼情況。

問題描述

如下圖,按照《字串連線你用+還是用StringBuilder》的程式碼在 javap 後發現它並沒有建立 StringBuilder 類和一些相應的操作,與文章的描述的並不符合,使用的JDK版本為JDK10。

image

image

問題原因

JDK9及以後的編譯器已經改成用動態指令執行位元組碼了,具體的呼叫實現在 java.lang.invoke.StringConcatFactory 類中,也就是說指令沒有生成到class檔案中儲存起來,而是執行時生成。 具體實現如下,有六種策略,前五種還是用StringBuilder實現。

《字串連線你用+還是用StringBuilder》續

JDK9前後

對於下面簡單的例子,JDK8及之前和JDK9及之後編譯的位元組碼有什麼差別

public class TestString2 {
	public static void main(String[] args) {
		String s = "www";
		for (int i = 0; i < 10; i++)
			s += i;
	}
}
複製程式碼

JDK8及之前,

public class com.seaboat.string.TestString2 {
  public com.seaboat.string.TestString2();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #16                 // String www
       2: astore_1
       3: iconst_0
       4: istore_2
       5: goto          30
       8: new           #18                 // class java/lang/StringBuilder
      11: dup
      12: aload_1
      13: invokestatic  #20                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
      16: invokespecial #26                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
      19: iload_2
      20: invokevirtual #29                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
      23: invokevirtual #33                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      26: astore_1
      27: iinc          2, 1
      30: iload_2
      31: bipush        10
      33: if_icmplt     8
      36: return
}
複製程式碼

JDK9及之後,

public class com.seaboat.string.TestString2 {
  public com.seaboat.string.TestString2();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String www
       2: astore_1
       3: iconst_0
       4: istore_2
       5: iload_2
       6: bipush        10
       8: if_icmpge     25
      11: aload_1
      12: iload_2
      13: invokedynamic #3,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;I)Ljava/lang/String;
      18: astore_1
      19: iinc          2, 1
      22: goto          5
      25: return
}
複製程式碼

InvokeDynamic

可以看到JDK9之後生成的位元組碼是比較簡潔的,只有一個 InvokeDynamic 指令,編譯器會給該類位元組碼增加 invokedynamic 指令相關內容,包括方法控制程式碼、引導方法、呼叫點、方法型別等等。它會呼叫 java.lang.invoke.StringConcatFactory 類中的makeConcatWithConstants方法,它有六種策略來處理字串。如下程式碼所示,有預設的策略,也可以通過java.lang.invoke.stringConcat啟動引數來修改策略。

private static final Strategy DEFAULT_STRATEGY = Strategy.MH_INLINE_SIZED_EXACT;

static {
        STRATEGY = DEFAULT_STRATEGY;

        Properties props = GetPropertyAction.privilegedGetProperties();
        final String strategy =
                props.getProperty("java.lang.invoke.stringConcat");
        CACHE_ENABLE = Boolean.parseBoolean(
                props.getProperty("java.lang.invoke.stringConcat.cache"));
        DEBUG = Boolean.parseBoolean(
                props.getProperty("java.lang.invoke.stringConcat.debug"));
        final String dumpPath =
                props.getProperty("java.lang.invoke.stringConcat.dumpClasses");

        STRATEGY = (strategy == null) ? DEFAULT_STRATEGY : Strategy.valueOf(strategy);
        CACHE = CACHE_ENABLE ? new ConcurrentHashMap<>() : null;
        DUMPER = (dumpPath == null) ? null : ProxyClassesDumper.getInstance(dumpPath);
    }
複製程式碼
private enum Strategy {
        BC_SB,
        BC_SB_SIZED,
        BC_SB_SIZED_EXACT,
        MH_SB_SIZED,
        MH_SB_SIZED_EXACT,
        MH_INLINE_SIZED_EXACT
    }
複製程式碼

有六種策略,前五種還是用StringBuilder實現,而預設的策略MH_INLINE_SIZED_EXACT,這種策略下是直接使用位元組陣列來操作,並且位元組陣列長度預先計算好,可以減少字串複製操作。實現的核心是通過 MethodHandle 來實現 runtime,具體實現邏輯在MethodHandleInlineCopyStrategy.generate方法中。

private static MethodHandle generate(Lookup lookup, String className, MethodType mt, Recipe recipe) throws StringConcatException {
        try {
            switch (STRATEGY) {
                case BC_SB:
                    return BytecodeStringBuilderStrategy.generate(lookup, className, mt, recipe, Mode.DEFAULT);
                case BC_SB_SIZED:
                    return BytecodeStringBuilderStrategy.generate(lookup, className, mt, recipe, Mode.SIZED);
                case BC_SB_SIZED_EXACT:
                    return BytecodeStringBuilderStrategy.generate(lookup, className, mt, recipe, Mode.SIZED_EXACT);
                case MH_SB_SIZED:
                    return MethodHandleStringBuilderStrategy.generate(mt, recipe, Mode.SIZED);
                case MH_SB_SIZED_EXACT:
                    return MethodHandleStringBuilderStrategy.generate(mt, recipe, Mode.SIZED_EXACT);
                case MH_INLINE_SIZED_EXACT:
                    return MethodHandleInlineCopyStrategy.generate(mt, recipe);
                default:
                    throw new StringConcatException("Concatenation strategy " + STRATEGY + " is not implemented");
            }
        } catch (Error | StringConcatException e) {
            throw e;
        } catch (Throwable t) {
            throw new StringConcatException("Generator failed", t);
        }
    }
複製程式碼

-------------推薦閱讀------------

我的開源專案彙總(機器&深度學習、NLP、網路IO、AIML、mysql協議、chatbot)

我的2017文章彙總——機器學習篇

我的2017文章彙總——Java及中介軟體

我的2017文章彙總——深度學習篇

我的2017文章彙總——JDK原始碼篇

我的2017文章彙總——自然語言處理篇

我的2017文章彙總——Java併發篇


跟我交流,向我提問:

《字串連線你用+還是用StringBuilder》續

公眾號的選單已分為“讀書總結”、“分散式”、“機器學習”、“深度學習”、“NLP”、“Java深度”、“Java併發核心”、“JDK原始碼”、“Tomcat核心”等,可能有一款適合你的胃口。

為什麼寫《Tomcat核心設計剖析》

歡迎關注:

《字串連線你用+還是用StringBuilder》續

相關文章