Вопрос 8 · Раздел 12

Как компилятор Java оптимизирует конкатенацию строк?

Компилятор применяет две оптимизации: constant folding (склеивание литералов на этапе компиляции) и генерацию эффективного байт-кода для переменных (StringBuilder в Java 8, invo...

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Компилятор применяет две оптимизации: constant folding (склеивание литералов на этапе компиляции) и генерацию эффективного байт-кода для переменных (StringBuilder в Java 8, invokedynamic в Java 9+). Если он видит, что строки известны заранее, он склеивает их ещё до запуска программы.

Пример:

String msg = "Hello" + " " + "World";
// Компилятор превращает это в:
String msg = "Hello World";
// Никакой работы во время выполнения — строка уже готова!

Если строки — переменные, компилятор использует StringBuilder (в старых версиях) или специальную фабрику StringConcatFactory (в новых), чтобы сделать операцию эффективной.

Простое правило: Для 2-3 строк в одной строке кода используйте +. Для цикла — StringBuilder.


🟡 Middle Level

Compile-time: Constant Folding

Компилятор (javac) вычисляет строковые выражения на этапе сборки, если все операнды — литералы или static final константы:

public static final String PREFIX = "LOG:";
String msg = PREFIX + " " + "Start";
// В байт-коде: String msg = "LOG: Start";

Но это НЕ работает с обычными переменными:

String prefix = "LOG:"; // НЕ final!
String msg = prefix + " Start";
// В байт-коде: runtime конкатенация

Java 5-8: StringBuilder

В этот период компилятор заменял каждое выражение a + b + c на:

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

Минус: байт-код “жестко зашит”. Если появится лучший способ — нужна перекомпиляция.

Java 9+: invokedynamic (JEP 280)

Компилятор больше не генерирует StringBuilder. Вместо этого — invokedynamic:

invokedynamic makeConcatWithConstants:(...)String

При первом выполнении JVM вызывает bootstrap-метод StringConcatFactory.makeConcatWithConstants(), который генерирует оптимальный код для данной конкретной конкатенации.

Типичные ошибки

  1. Ошибка: Думать, что компилятор оптимизирует циклы Решение: Оптимизация работает только в рамках одного выражения

  2. Ошибка: null + строка без проверки Решение: Результат — "null...". Используйте тернарный оператор или Objects.toString()


🔴 Senior Level

Internal Implementation — StringConcatFactory

StringConcatFactory (java.lang.invoke) — сердце конкатенации в Java 9+. Bootstrap-метод:

public static CallSite makeConcatWithConstants(MethodHandles.Lookup lookup,
                                                String name,
                                                MethodType concatType,
                                                MethodRecipe recipe,
                                                byte[] constants) {
    // recipe — "рецепт" конкатенации: какие аргументы и в каком порядке соединять.
    // constants — константы, которые встраиваются между аргументами.
    // Выбирает стратегию:
    // 1. MH_LF — MethodHandle-based
    // 2. BC_SB — Bytecode StringBuilder
    // 3. BH_SB — Bytecode StringBuilder compact
    // Генерирует optimal код для данной сигнатуры
}

Стратегии: | Стратегия | Механизм | Когда выбирается | | ———– | ——————————— | ————————— | | MH_LF | MethodHandle с inline lambda | Few arguments, simple types | | BC_SB | Генерация байт-кода StringBuilder | Many arguments, mixed types | | BH_SB | Compact bytecode | Default fallback |

Преимущества invokedynamic перед StringBuilder codegen

  1. Adaptivity: Разные версии JVM могут использовать разные стратегии без перекомпиляции .class

  2. Zero intermediate allocations: JVM знает все аргументы заранее и может выделить byte[] нужного размера сразу:
    // Вместо: new StringBuilder(16).append(a).append(b).append(c).toString()
    // Генерируется: прямой расчёт размера → byte[resultSize] → fill → new String
    
  3. Compactness: Байт-код классов стал меньше (нет раздутого StringBuilder-кода)

  4. Type-aware конвертация: Для примитивов (int, long) генерируется прямая конвертация без String.valueOf() промежуточного объекта.

Архитектурные Trade-offs

Плюсы:

  • Future-proof: новые стратегии JVM без перекомпиляции
  • Меньше аллокаций → меньше GC pressure
  • Более компактный байт-код

Минусы:

  • Bootstrap overhead: первый вызов ~1-5μs (JIT компиляция сгенерированного кода)
  • Сложнее профилировать: динамически генерированный код не виден в декомпиляторе

Edge Cases

  1. null handling: StringConcatFactory встраивает проверку на null. Результат: "text: null" вместо NPE.

  2. Constant folding + invokedynamic: Если часть аргументов — константы, StringConcatFactory получает их как baked-in constants в recipe, избегая передачи как аргументов.

  3. Inlining: После bootstrap JIT может inline-ить сгенерированный код, убирая виртуальный вызов.

Производительность

| Сценарий | 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

Сценарий: REST API сериализация (JSON ключи + значения) — 200K вызовов/сек:

  • Java 8: 200K new StringBuilder() → ~40ms CPU overhead
  • Java 9+: invokedynamic с прямой аллокацией → ~15ms
  • С -XX:+UseStringDeduplication: дополнительно -10% heap от дублирующихся ключей

Monitoring

# JIT-компиляция StringConcatFactory
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+LogCompilation ...

# JOL для анализа аллокаций
GraphLayout.parseInstance(result).toFootprint()

Best Practices для Highload

  • Для простых конкатенаций: + — JVM оптимизирует лучше ручного StringBuilder
  • В циклах: только StringBuilder (или StringBuilder вне цикла)
  • Для форматирования: String.format() медленнее (парсит шаблон). Альтернативы: StringBuilder, MessageFormat, или StringTemplate (Java 21+, preview)
  • Если конкатенация может не понадобиться (lazy logging): оберните в if (log.isDebugEnabled())

🎯 Шпаргалка для интервью

Обязательно знать:

  • Constant folding: "A" + "B" и static final константы склеиваются на этапе компиляции
  • Java 5-8: a + b + cnew StringBuilder().append(a).append(b).append(c).toString()
  • Java 9+: invokedynamic + StringConcatFactory — JVM выбирает стратегию в рантайме без перекомпиляции
  • Стратегии: MH_LF (MethodHandle), BC_SB (Bytecode StringBuilder), BH_SB (Compact)
  • invokedynamic даёт zero intermediate allocations — JVM знает размер и аллоцирует byte[] сразу
  • Bootstrap overhead первого вызова: ~1-5μs, затем JIT инлайнит

Частые уточняющие вопросы:

  • Почему invokedynamic лучше StringBuilder codegen? — (1) Adaptivity — JVM может менять стратегию без перекомпиляции, (2) Меньше аллокаций — прямой расчёт размера, (3) Компактнее байт-код, (4) Type-aware для примитивов.
  • Работает ли constant folding для обычных переменных? — Нет, только для литералов и static final констант. String x = "A"; x + "B" — не constant folding.
  • Какой overhead у bootstrap invokedynamic? — ~1-5μs при первом вызове. JIT затем инлайнит сгенерированный код.
  • Оптимизирует ли компилятор null + "text"? — Да, результат "nulltext". StringConcatFactory встраивает null check.

Красные флаги (НЕ говорить):

  • ❌ “Компилятор оптимизирует + в цикле” — нет, только одно выражение
  • ❌ “invokedynamic — это то же самое что StringBuilder” — адаптивный, меньше аллокаций, future-proof
  • ❌ “Constant folding работает для любых переменных” — только для static final
  • ❌ “Bootstrap overhead происходит каждый раз” — только при первом вызове, затем JIT инлайнит

Связанные темы:

  • [[7. Что происходит при конкатенации строк через оператор +]]
  • [[5. Когда использовать StringBuilder, а когда StringBuffer]]
  • [[19. Что такое compact strings в Java 9+]]