Как компилятор Java оптимизирует конкатенацию строк?
Компилятор применяет две оптимизации: constant folding (склеивание литералов на этапе компиляции) и генерацию эффективного байт-кода для переменных (StringBuilder в Java 8, invo...
🟢 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(), который генерирует оптимальный код для данной конкретной конкатенации.
Типичные ошибки
-
Ошибка: Думать, что компилятор оптимизирует циклы Решение: Оптимизация работает только в рамках одного выражения
-
Ошибка:
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
-
Adaptivity: Разные версии JVM могут использовать разные стратегии без перекомпиляции
.class - Zero intermediate allocations: JVM знает все аргументы заранее и может выделить
byte[]нужного размера сразу:// Вместо: new StringBuilder(16).append(a).append(b).append(c).toString() // Генерируется: прямой расчёт размера → byte[resultSize] → fill → new String -
Compactness: Байт-код классов стал меньше (нет раздутого StringBuilder-кода)
- Type-aware конвертация: Для примитивов (
int,long) генерируется прямая конвертация безString.valueOf()промежуточного объекта.
Архитектурные Trade-offs
Плюсы:
- Future-proof: новые стратегии JVM без перекомпиляции
- Меньше аллокаций → меньше GC pressure
- Более компактный байт-код
Минусы:
- Bootstrap overhead: первый вызов ~1-5μs (JIT компиляция сгенерированного кода)
- Сложнее профилировать: динамически генерированный код не виден в декомпиляторе
Edge Cases
-
null handling:
StringConcatFactoryвстраивает проверку на null. Результат:"text: null"вместо NPE. -
Constant folding + invokedynamic: Если часть аргументов — константы,
StringConcatFactoryполучает их как baked-in constants в recipe, избегая передачи как аргументов. -
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 + c→new 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+]]