Question 8 · Section 12

How Java Compiler Optimizes String Concatenation?

The compiler applies two optimizations: constant folding (gluing literals at compile time) and generating efficient bytecode for variables (StringBuilder in Java 8, invokedynami...

Language versions: English Russian Ukrainian

🟢 Junior Level

The compiler applies two optimizations: constant folding (gluing literals at compile time) and generating efficient bytecode for variables (StringBuilder in Java 8, invokedynamic in Java 9+). If it sees strings known in advance, it glues them together before the program runs.

Example:

String msg = "Hello" + " " + "World";
// Compiler turns this into:
String msg = "Hello World";
// No runtime work — the string is ready!

If strings are variables, the compiler uses StringBuilder (in older versions) or a special StringConcatFactory factory (in newer ones) to make the operation efficient.

Simple rule: For 2-3 strings on one line of code, use +. For a loop — StringBuilder.


🟡 Middle Level

Compile-time: Constant Folding

The compiler (javac) computes string expressions at build time if all operands are literals or static final constants:

public static final String PREFIX = "LOG:";
String msg = PREFIX + " " + "Start";
// In bytecode: String msg = "LOG: Start";

But this does NOT work with regular variables:

String prefix = "LOG:"; // NOT final!
String msg = prefix + " Start";
// In bytecode: runtime concatenation

Java 5-8: StringBuilder

During this period the compiler replaced every a + b + c expression with:

new StringBuilder().append(a).append(b).append(c).toString()

Downside: bytecode is “hardcoded”. If a better approach appears — recompilation is needed.

Java 9+: invokedynamic (JEP 280)

The compiler no longer generates StringBuilder. Instead — invokedynamic:

invokedynamic makeConcatWithConstants:(...)String

On first execution, JVM calls the bootstrap method StringConcatFactory.makeConcatWithConstants(), which generates optimal code for this specific concatenation.

Typical mistakes

  1. Mistake: Thinking the compiler optimizes loops Solution: Optimization works only within a single expression

  2. Mistake: null + string without check Solution: Result is "null...". Use ternary operator or Objects.toString()


🔴 Senior Level

Internal Implementation — StringConcatFactory

StringConcatFactory (java.lang.invoke) — the heart of concatenation in Java 9+. Bootstrap method:

public static CallSite makeConcatWithConstants(MethodHandles.Lookup lookup,
                                                String name,
                                                MethodType concatType,
                                                MethodRecipe recipe,
                                                byte[] constants) {
    // recipe — concatenation "recipe": which arguments and in what order to join.
    // constants — constants embedded between arguments.
    // Chooses strategy:
    // 1. MH_LF — MethodHandle-based
    // 2. BC_SB — Bytecode StringBuilder
    // 3. BH_SB — Bytecode StringBuilder compact
    // Generates optimal code for this signature
}

Strategies: | Strategy | Mechanism | When chosen | | ——– | ——————————– | ———————- | | MH_LF | MethodHandle with inline lambda | Few arguments, simple types | | BC_SB | StringBuilder bytecode generation| Many arguments, mixed types | | BH_SB | Compact bytecode | Default fallback |

Advantages of invokedynamic over StringBuilder codegen

  1. Adaptivity: Different JVM versions can use different strategies without recompiling .class

  2. Zero intermediate allocations: JVM knows all arguments in advance and can allocate byte[] of the right size immediately:
    // Instead of: new StringBuilder(16).append(a).append(b).append(c).toString()
    // Generated: direct size calculation → byte[resultSize] → fill → new String
    
  3. Compactness: Class bytecode became smaller (no bloated StringBuilder code)

  4. Type-aware conversion: For primitives (int, long) direct conversion is generated without intermediate String.valueOf() object.

Architectural Trade-offs

Pros:

  • Future-proof: new JVM strategies without recompilation
  • Fewer allocations → less GC pressure
  • More compact bytecode

Cons:

  • Bootstrap overhead: first call ~1-5μs (JIT compilation of generated code)
  • Harder to profile: dynamically generated code is not visible in decompiler

Edge Cases

  1. null handling: StringConcatFactory embeds a null check. Result: "text: null" instead of NPE.

  2. Constant folding + invokedynamic: If some arguments are constants, StringConcatFactory receives them as baked-in constants in the recipe, avoiding passing them as arguments.

  3. Inlining: After bootstrap, JIT may inline the generated code, removing the virtual call.

Performance

| Scenario | Java 8 (StringBuilder) | Java 9+ (invokedynamic) | Improvement | | ———————- | ———————- | ———————– | ———– | | 2 String args | ~25ns | ~10ns | 2.5x | | 5 mixed args | ~60ns | ~25ns | 2.4x | | 10 String args | ~120ns | ~45ns | 2.7x | | First call (bootstrap) | N/A | +3-5μs | One-time cost |

Production Experience

Scenario: REST API serialization (JSON keys + values) — 200K calls/sec:

  • Java 8: 200K new StringBuilder() → ~40ms CPU overhead
  • Java 9+: invokedynamic with direct allocation → ~15ms
  • With -XX:+UseStringDeduplication: additional -10% heap from duplicate keys

Monitoring

# JIT compilation of StringConcatFactory
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+LogCompilation ...

# JOL for allocation analysis
GraphLayout.parseInstance(result).toFootprint()

Best Practices for Highload

  • For simple concatenations: + — JVM optimizes better than manual StringBuilder
  • In loops: only StringBuilder (or StringBuilder outside the loop)
  • For formatting: String.format() is slower (parses template). Alternatives: StringBuilder, MessageFormat, or StringTemplate (Java 21+, preview)
  • If concatenation may not be needed (lazy logging): wrap in if (log.isDebugEnabled())

🎯 Interview Cheat Sheet

Must know:

  • Constant folding: "A" + "B" and static final constants are glued at compile time
  • Java 5-8: a + b + cnew StringBuilder().append(a).append(b).append(c).toString()
  • Java 9+: invokedynamic + StringConcatFactory — JVM chooses strategy at runtime without recompilation
  • Strategies: MH_LF (MethodHandle), BC_SB (Bytecode StringBuilder), BH_SB (Compact)
  • invokedynamic gives zero intermediate allocations — JVM knows size and allocates byte[] immediately
  • Bootstrap overhead of first call: ~1-5μs, then JIT inlines

Frequent follow-up questions:

  • Why is invokedynamic better than StringBuilder codegen? — (1) Adaptivity — JVM can change strategy without recompilation, (2) Fewer allocations — direct size calculation, (3) More compact bytecode, (4) Type-aware for primitives.
  • Does constant folding work for regular variables? — No, only for literals and static final constants. String x = "A"; x + "B" — not constant folding.
  • What’s the bootstrap invokedynamic overhead? — ~1-5μs on first call. JIT then inlines the generated code.
  • Does the compiler optimize null + "text"? — Yes, result is "nulltext". StringConcatFactory embeds a null check.

Red flags (DON’T say):

  • ❌ “Compiler optimizes + in loops” — no, only a single expression
  • ❌ “invokedynamic — the same as StringBuilder” — adaptive, fewer allocations, future-proof
  • ❌ “Constant folding works for any variables” — only for static final
  • ❌ “Bootstrap overhead happens every time” — only on first call, then JIT inlines

Related topics:

  • [[7. What Happens When Concatenating Strings with + Operator]]
  • [[5. When to Use StringBuilder vs StringBuffer]]
  • [[19. What are Compact Strings in Java 9+]]