It's explained in the JEP. Emphasis mine

java.base exemptions. Since this feature uses a core library feature (java.lang.invoke) to implement a core language feature (String concatenation), we have to exempt the java.base module from using the indified String concat. Otherwise a circularity happens when the java.lang.invoke.* machinery requires String concat to work, which in turn requires the java.lang.invoke.* machinery. This exemption may limit the performance improvements observed from this feature, since many java.base classes will not be able to use it. We consider this an acceptable downside, which should be covered by the VM's optimizing compilers.

Answer from Michael on Stack Overflow
🌐
Oracle
docs.oracle.com › en › java › javase › 17 › docs › api › java.base › java › lang › StringBuilder.html
StringBuilder (Java SE 17 & JDK 17)
April 21, 2026 - The principal operations on a StringBuilder are the append and insert methods, which are overloaded so as to accept data of any type. Each effectively converts a given datum to a string and then appends or inserts the characters of that string to the string builder.
🌐
Oracle
docs.oracle.com › en › java › javase › 17 › docs › api › › java.base › java › lang › class-use › StringBuilder.html
Uses of Class java.lang.StringBuilder (Java SE 17 & JDK 17)
October 20, 2025 - Provides classes that are fundamental to the design of the Java programming language. ... Classes for matching character sequences against patterns specified by regular expressions. ... Appends the specified StringBuffer to this sequence. ... Compares two StringBuilder instances lexicographically.
🌐
GitHub
github.com › openjdk-mirror › jdk7u-jdk › blob › master › src › share › classes › java › lang › StringBuilder.java
jdk7u-jdk/src/share/classes/java/lang/StringBuilder.java at master · openjdk-mirror/jdk7u-jdk
* @see java.lang.String · * @since 1.5 · */ public final class StringBuilder · extends AbstractStringBuilder · implements java.io.Serializable, CharSequence · { · /** use serialVersionUID for interoperability */ static final long serialVersionUID = 4383685877147921099L; ·
Author   openjdk-mirror
Top answer
1 of 1
1

Taking @Holger's advice, I checked the bytecode for following code:

public class StringBM {
    public String toStringPlus(String a) {
        return "{a:" + a + ", b:" + ", c: " + "}";
    }

    public String toStringBuilder(String a) {
        StringBuilder sb = new StringBuilder(100);
        return sb.append("{a:").append(a)
                .append(", b:")
                .append(", c:")
                .append("}")
                .toString();
    }
}

For toStringPlus, I get

  public java.lang.String toStringPlus(java.lang.String);
    descriptor: (Ljava/lang/String;)Ljava/lang/String;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: aload_1
         1: invokedynamic #7,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
         6: areturn

for toStringBuilder:

public java.lang.String toStringBuilder(java.lang.String);
    descriptor: (Ljava/lang/String;)Ljava/lang/String;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=2
         0: new           #11                 // class java/lang/StringBuilder
         3: dup
         4: bipush        100
         6: invokespecial #13                 // Method java/lang/StringBuilder."<init>":(I)V
         9: astore_2
        10: aload_2
        11: ldc           #16                 // String {a:
        13: invokevirtual #18                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        16: aload_1
        17: invokevirtual #18                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        20: ldc           #22                 // String , b:
        22: invokevirtual #18                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        25: ldc           #24                 // String , c:
        27: invokevirtual #18                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        30: ldc           #26                 // String }
        32: invokevirtual #18                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        35: invokevirtual #28                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        38: areturn

The + version simply invokes dynamic function makeConcatWithConstants.
Whereas the StringBuilder version has to do it the 'honest' way.
I guess we can see why is + faster now.

🌐
Oracle
docs.oracle.com › en › java › javase › 17 › docs › api › java.base › java › lang › String.html
String (Java SE 17 & JDK 17)
April 21, 2026 - The implementation of the string concatenation operator is left to the discretion of a Java compiler, as long as the compiler ultimately conforms to The Java Language Specification. For example, the javac compiler may implement the operator with StringBuffer, StringBuilder, or java.lang.invoke.StringConcatFactory depending on the JDK version.
🌐
Medium
medium.com › javarevisited › string-handling-in-java-day-17-mastering-strings-and-stringbuilder-e603d92718fd
String Handling in Java: Day 17 – Mastering Strings and StringBuilder | by Krishna | Javarevisited | Medium
March 8, 2024 - In Java, String is an immutable class, meaning once a string object is created, its content cannot be changed. On the other hand, StringBuilder is mutable and provides an efficient way to modify strings.
🌐
Hyperskill
hyperskill.org › university › java › java-stringbuilder
Java StringBuilder
December 4, 2024 - 17 projects · 3K already learning · Strings in Java are immutable which means that once created, a string cannot be changed. If we want to modify the content of a string object, we should create a new string. This may not be the best way when we perform a lot of modifications because each operation creates a new object, which is bad for performance. Fortunately, there is a special class named StringBuilder that is used to create mutable string objects.
Find elsewhere
🌐
Tutorialspoint
tutorialspoint.com › java › lang › java_lang_stringbuilder.htm
Java StringBuilder Class
package com.tutorialspoint; public class StringBuilderDemo { public static void main(String[] args) { StringBuilder stringBuilder = new StringBuilder("tuts "); System.out.println("builder = " + stringBuilder); // appends the boolean argument as string to the string stringBuilder stringBuilder.append(true); // print the string stringBuilder after appending System.out.println("After append = " + stringBuilder); stringBuilder = new StringBuilder("abcd "); System.out.println("stringBuilder = " + stringBuilder); // appends the boolean argument as string to the string stringBuilder stringBuilder.append(false); // print the string stringBuilder after appending System.out.println("After append = " + stringBuilder); } }
🌐
Oracle
docs.oracle.com › en › java › javase › 18 › docs › api › java.base › java › lang › StringBuilder.html
StringBuilder (Java SE 18 & JDK 18)
August 18, 2022 - The principal operations on a StringBuilder are the append and insert methods, which are overloaded so as to accept data of any type. Each effectively converts a given datum to a string and then appends or inserts the characters of that string to the string builder.
🌐
ZetCode
zetcode.com › java › string
Java String - working with strings in Java using String and StringBuilder
July 4, 2024 - Java String tutorial shows how to work with strings in Java using String and StringBuilder. In Java, a string is a sequence of Unicode characters.
🌐
Classpath
developer.classpath.org › doc › java › lang › StringBuilder-source.html
Source for java.lang.StringBuilder (GNU Classpath 0.95 Documentation)
Note that this has permissions set this way so that String 99: * can get the value. 100: * 101: * @serial the buffer 102: */ 103: char[] value; 104: 105: /** 106: * The default capacity of a buffer. 107: */ 108: private static final int DEFAULT_CAPACITY = 16; 109: 110: /** 111: * Create a new StringBuilder with default capacity 16.
🌐
GitHub
github.com › AdoptOpenJDK › openjdk-jdk11 › blob › master › src › java.base › share › classes › java › lang › StringBuilder.java
openjdk-jdk11/src/java.base/share/classes/java/lang/StringBuilder.java at master · AdoptOpenJDK/openjdk-jdk11
* <p>Instances of {@code StringBuilder} are not safe for · * use by multiple threads. If such synchronization is required then it is · * recommended that {@link java.lang.StringBuffer} be used. * * <p>Unless otherwise noted, passing a {@code null} argument to a constructor ·
Author   AdoptOpenJDK
🌐
Oracle
docs.oracle.com › en › java › javase › 17 › docs › api › java.base › java › lang › StringBuffer.html
StringBuffer (Java SE 17 & JDK 17)
April 21, 2026 - As of release JDK 5, this class has been supplemented with an equivalent class designed for use by a single thread, StringBuilder.