专栏名称: 高可用架构
高可用架构公众号。
目录
相关文章推荐
架构师之路  ·  巧用CAS,一分钟实现分布式ID生成器!(第 ... ·  23 小时前  
架构师之路  ·  我升级了,欢迎大家试用 ·  3 天前  
美团技术团队  ·  老显卡福音!美团开源首发INT8无损满血版D ... ·  4 天前  
51好读  ›  专栏  ›  高可用架构

Java字符串拼接技术演进及阿里巴巴的贡献

高可用架构  · 公众号  · 架构  · 2024-08-30 08:17

正文

0. 写在前面的省流版

下图是Java字符串拼接实现的技术演进路线,最新的实现 PR 20273是来自阿里巴巴的贡献。

1. 关于使用"+"做字符串拼接

一些古老的技术文章中会说,在Java中使用"+"做字符串拼接性能不好,但实际情况是JDK 9+之后的版本,使用"+"做字符串拼接会比StringBuilder快。

如下是一个字符串拼接的的方法,我们基于这个方法来介绍JDK8和JDK9之后版本的性能以及背后的内部细节。

class Demo {    public static String concatIndy(int i) {        return  "value " + i;    }}

2. JDK 8下的字符串拼接实现


2.1 编译并查看字节码

jdk8/bin/javac Demo.java jdk8/bin/javap -c Dem
javap输出的字节码:
class Demo {  Demo();    Code:       0: aload_0       1: invokespecial #1 // Method java/lang/Object."":()V       4: return
 public static java.lang.String concatIndy(int);    Code:       0: new           #2 // class java/lang/StringBuilder       3: dup       4: invokespecial #3 // Method java/lang/StringBuilder."":()V       7: ldc           #4 // String value       9: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;      12: iload_0      13: invokevirtual #6 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;      16: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;      19: areturn}

2.2 反编译后的Java代码

public static String concatIndy(int i) {    return new StringBuilder("value ")            .append(i)            .toString();}

可以看出,在JDK 8中,在非循环体内使用"+"实现字符串拼接和使用StringBuilder是一样的,用"+"做拼接代码更简洁,推荐使用"+"而不是StringBuilder。

3. JDK 9之后的字符串拼接实现 (JEP 280)

如果我们使用JDK 9之后的版本,比如JDK 11,字符串拼接"+"编译后的字节码会不一样。

3.1. 使用JDK 11编译后并查看字节码

jdk11/bin/javac Demo.java jdk11/bin/javap -c Demo 
class Demo {  Demo();    Code:       0: aload_0       1: invokespecial #1 // Method java/lang/Object."":()V       4: return
 public static java.lang.String concatIndy(int);    Code:       0: iload_0       1: invokedynamic #2,  0 // InvokeDynamic #0:makeConcatWithConstants:(I)Ljava/lang/String;       6: areturn}

可以看到,JDK 11中编译后的字节码和JDK 8是不一样的,不再是基于StringBuilder实现,而是基于StringConcatFactory.makeConcatWithConstants动态生成一个方法来实现。这个会比StringBuilder更快,不需要创建StringBuilder对象,也会减少一次数组拷贝。

这里由于是内部使用的数组,所以用了UNSAFE.allocateUninitializedArray的方式更快分配byte[]数组。 通过:

StringConcatFactory.makeConcatWithConstants

而不是JavaC生成代码,是因为生成的代码无法使用JDK的内部方法进行优化,还有就是,如果有算法变化,存量的Lib不需要重新编译,升级新版本JDk就能提速。
这个字节码相当如下手工调用:
StringConcatFactory.makeConcatWithConstants
import java.lang.invoke.*;
   static final MethodHandle STR_INT;
   static {        try {            STR_INT = StringConcatFactory.makeConcatWithConstants(                    MethodHandles.lookup(),                    "concat_str_int",                    MethodType.methodType(String.class, int.class),                    "value \1"            ).dynamicInvoker();        } catch (Exception e) {            throw new Error("Bootstrap error", e);        }    }
   static String concat_str_int(int value) throws Throwable {        return (String) STR_INT.invokeExact(value);    }

StringConcatFactory.makeConcatWithConstants是公开API,可以用来动态生成字符串拼接的方法,除了编译器生成字节码调用,也可以直接调用。调用生成方法一次大约需要1微秒(千分之一毫秒)。

3.2. makeConcatWithConstants动态生成方法的代码

makeConcatWithConstants使用recipe ("value \1")动态生成的方法大致如下:
import java.lang.StringConcatHelper;import static java.lang.StringConcatHelper.mix;import static java.lang.StringConcatHelper.newArray;import static java.lang.StringConcatHelper.prepend;import static java.lang.StringConcatHelper.newString;
public static String invokeStatic(String str, int value) throws Throwable { long lengthCoder = 0; lengthCoder = mix(lengthCoder, str); lengthCoder = mix(lengthCoder, value); byte[] bytes = newArray(lengthCoder); lengthCoder = prepend(lengthCoder, bytes, value); lengthCoder = prepend(lengthCoder, bytes, str); return newString(bytes, lengthCoder);}
  • StringConcatHelper

StringConcatHelper是:

StringConcatFactory.makeConcatWithConstants实现用到的内部类。
package java.lang;
class StringConcatHelper {     static String newString(byte[] buf, long indexCoder) {        // Use the private, non-copying constructor (unsafe!)        if (indexCoder == LATIN1) {            return new String(buf, String.LATIN1);        } else if (indexCoder == UTF16) {            return new String(buf, String.UTF16);        }    }}

public class String {    String(byte[] value, byte coder) {        // 无拷贝构造        this.value = value;        this.coder = coder;    }}
可以看出,生成的方法是通过如下步骤来实现:
  1. StringConcatHelper的mix方法计算长度和字符编码 (将长度和coder组合放到一个long中);

  2. 根据长度和编码构造一个byte[];

  3. 然后把相关的值写入到byte[]中;

  4. 使用byte[]无拷贝的方式构造String对象。

上面的火焰图可以看到实现的细节。这样的实现,和使用StringBuilder相比,减少了StringBuilder以及StringBuilder内部byte[]对象的分配,可以减轻GC的负担。也能避免可能产生的StringBuilder在latin1编码到UTF16时的数组拷贝。
-StringBuilder缺省编码是LATIN1(ISO_8859_1),如果append过程中遇到UTF16编码,会有一个将LATIN1转换为UTF16的动作,这个动作实现的方法是inflate。如果拼接的参数如果是带中文的字符串,使用StringBuilder还会多一次数组拷贝。
class AbstractStringBuilder    private void inflate() {        if (!isLatin1()) {            return;        }        byte[] buf = StringUTF16.newBytesFor(value.length);        StringLatin1.inflate(value, 0, buf, 0, count);        this.value = buf;        this.coder = UTF16;    }}


3.3 JMH比较字符串拼接和使用StringBuilder的性能

  • 测试代码
public class ConcatBench {    public static String concatIndy(int i) {        return  "value " + i;    }    public static String concatSB(int i) {        return new StringBuilder("value ")                .append(i)                .toString();    }}
  • JDK 11下的测试结果
Benchmark                Mode  Cnt    Score   Error   UnitsConcatBench.concatIndy  thrpt    5  130.841 ± 1.127




    
  ops/usConcatBench.concatSB    thrpt    5  117.897 ± 1.437  ops/us
上面的数据中,concatIndy是使用字符串拼接,concatSB是使用StringBuilder,可以看出在JDK 11下,使用"+"拼接字符串的性能比使用StringBuilder快10.97%。

4. StringConcatFactory的实现细节

以及阿里巴巴的贡献

4.1 基于MethodHandlers API的实现

StringConcatFactory.makeConcatWithConstants是MethodHandles实现的。MethodHandle可以是一个方法引用,MethodHandles可以对MethodHandle做各种转换,包括过滤参数[filterAgument]、参数折叠[foldArgument]、添加参数[insertArguments],最终生成的MethodHandle可以被认为是一个语法树。MethodHandles API功能强大,甚至可以认为它是图灵完备的。当然也有缺点,复杂的MethodHandle TreeExpress会生成大量中的中间类,JIT的开销也较大。
StringConcatFactory.makeConcatWithConstants通过MethodHandles动态构建一个MethodHandle调用StringConcatHelper的方法,组装一个MethodHandle实现无拷贝的字符拼接实现。
package java.lang;
class StringConcatHelper {    static long initialCoder() { ... }    // T: boolean, char, int, long, String    static long mix(long lengthCoder, T value) { ... }    static byte[] newArray(long indexCoder) { ... }    static long prepend(long lengthCoder, byte[] buf, T value) { ... }    static String newString(byte[] buf, long indexCoder) { ... }}
class StringConcatFactory {  static MethodHandle generateMHInlineCopy(MethodType mt, String[] constants) {    Class>[] ptypes = mt.erase().parameterArray();     MethodHandle mh = MethodHandles.dropArgumentsTrusted(newString(), 2, ptypes);    ..    mh = filterInPrependers(mh, constants, ptypes);    ..    MethodHandle newArrayCombinator = newArray();    mh = MethodHandles.foldArgumentsWithCombiner(mh, 0, newArrayCombinator,            1 // index    );    ..    mh = filterAndFoldInMixers(mh, initialLengthCoder, ptypes);    if (objFilters != null) {      mh = MethodHandles.filterArguments(mh, 0, objFilters);    }    return mh;  }}

4.2 基于MethodHandle表达式的问题

这种动态生成MethodHandle表达式在参数个数较多时,会遇到问题,它会生成大量中间转换类,并且生成MethodHandle消耗比较大,极端情况下,C2优化器需要高达2G的内存来编译复杂的字符串拼接:

https://github.com/openjdk/jdk/pull/18953

因此JDK 23引入了JVM启动参数java.lang.invoke.StringConcat.highArityThreshold,缺省值为20,当超过这个阈值时,使用StringBuilder实现。
除了参数个数较多时编译消耗资源多之外,MethodHandle表达式还有启动速度比较慢的问题。

4.3 阿里巴巴贡献的改进 (PR 20273)

阿里巴巴的工程师温绍锦在2024年7月提交了一个新的方案:

https://github.com/openjdk/jdk/pull/20273
通过动态字节码生成隐藏类来代替基于MethodHandle表达式的实现。它的实现逻辑和MethodHandle表达式类似,但不需要组合coder和length,有相似的运行性能,但它能显著提升启动性能,并且对C2优化器的开销较小。
在8月5日召开的JVM Language Summit 2024,Oracle Java核心类库维护小组负责性能优化的Claes Redestad的Talk:

《Re-thinking String Concatenation》 :

https://www.youtube.com/watch?v=tgX38gvMpjs&list=PLX8CzqL3ArzUEYnTa6KYORRbP3nhsK0L1&index=1
介绍了 PR 20273 ,这个将会是JDK 24的缺省实现。

如下是PR 20273前后的代码实现对比:

上图右边的字节码会用下面的代码生成一个Hidden Class来实现字符串拼接。
package java.lang.invoke;
class StringConcatFactory {    static final class InlineHiddenClassStrategy {        static MethodHandle generate(Lookup lookup, MethodType args, String[] constants) {            byte[] classBytes = ClassFile.of().build(...);
           var hiddenClass = lookup.makeHiddenClassDefiner(CLASS_NAME, classBytes, Set.of(), DUMPER)                                    .defineClass(true, null);            var constructor = lookup.findConstructor(hiddenClass, CONSTRUCTOR_METHOD_TYPE);            var concat      = lookup.findVirtual(hiddenClass, METHOD_NAME, concatArgs);
           var instance = hiddenClass.cast(constructor.invoke(constants));            return concat.bindTo(instance);        }    }}
如下图,PR 20273的实现生成的中间类明显减少:
PR 20273生成的ByteCode逻辑大致如下:
  • java.lang.StringConcatHelper中的基类 StringConcatBase。
package java.lang;
class StringConcatHelper {    static abstract class StringConcatBase {        @Stable        final String[] constants;        final int      length;        final byte     coder;
       StringConcatBase(String[] constants) {            int  length = 0;            byte coder  = String.LATIN1;            for (String c : constants) {                length += c.length();                coder  |= c.coder();            }            this.constants = constants;            this .length    = length;            this.coder     = coder;        }    }}
  • 生成的代码。
import static java.lang.StringConcatHelper.newArrayWithSuffix; import static java.lang.StringConcatHelper.prepend; import static java.lang.StringConcatHelper.stringCoder; import static java.lang.StringConcatHelper.stringSize;  
class StringConcat extends java.lang.StringConcatHelper.StringConcatBase {    // super class defines        // String[] constants;        // int length;        // byte coder;          StringConcat(String[] constants) {        super(constants);    }        String concat(int arg0, long arg1, boolean arg2, char arg3, String arg4,                  float arg5, double arg6, Object arg7    ) {        // Types other than byte/short/int/long/boolean/String require a local variable to store        String str4 = stringOf(arg4);        String str5 = stringOf(arg5);        String str6 = stringOf(arg6);        String str7 = stringOf(arg7);        int coder  = coder(this.coder, arg0, arg1, arg2, arg3, str4, str5, str6, str7);        int length = length(this.length, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7);        String[] constants = this.constants;        byte[] buf = newArrayWithSuffix(constants[paramCount], length. coder);        prepend(length, coder, buf, constants, arg0, arg1, arg2, arg3, str4, str5, str6, str7);        return new String(buf, coder);    }        static int length(int length, int arg0, long arg1, boolean arg2, char arg3,                      String arg4, String arg5, String arg6, String arg7) {        return stringSize(stringSize(stringSize(stringSize(stringSize(stringSize(stringSize(stringSize(            length, arg0), arg1), arg2), arg3), arg4), arg5), arg6), arg7);    }        static int cocder(int coder, char arg3, String str4, String str5, String str6, String str7) {        return coder | stringCoder(arg3) | str4.coder() | str5.coder() | str6.coder() | str7.coder();    }        static int prepend(int length, int coder, byte[] buf, String[] constants,                       int arg0, long arg1, boolean arg2, char arg3,                       String str4, String str5, String str6, String str7) {        // StringConcatHelper. prepend






请到「今天看啥」查看全文