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...
🟢 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
-
Mistake: Thinking the compiler optimizes loops Solution: Optimization works only within a single expression
-
Mistake:
null+ string without check Solution: Result is"null...". Use ternary operator orObjects.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
-
Adaptivity: Different JVM versions can use different strategies without recompiling
.class - 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 -
Compactness: Class bytecode became smaller (no bloated StringBuilder code)
- Type-aware conversion: For primitives (
int,long) direct conversion is generated without intermediateString.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
-
null handling:
StringConcatFactoryembeds a null check. Result:"text: null"instead of NPE. -
Constant folding + invokedynamic: If some arguments are constants,
StringConcatFactoryreceives them as baked-in constants in the recipe, avoiding passing them as arguments. -
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(orStringBuilderoutside the loop) - For formatting:
String.format()is slower (parses template). Alternatives:StringBuilder,MessageFormat, orStringTemplate(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"andstatic finalconstants are glued at compile time - Java 5-8:
a + b + c→new 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)
invokedynamicgives zero intermediate allocations — JVM knows size and allocatesbyte[]immediately- Bootstrap overhead of first call: ~1-5μs, then JIT inlines
Frequent follow-up questions:
- Why is
invokedynamicbetter 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 finalconstants.String x = "A"; x + "B"— not constant folding. - What’s the bootstrap
invokedynamicoverhead? — ~1-5μs on first call. JIT then inlines the generated code. - Does the compiler optimize
null + "text"? — Yes, result is"nulltext".StringConcatFactoryembeds 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+]]