Як компілятор Java оптимізує конкатенацію рядків?
Компілятор застосовує дві оптимізації: constant folding (склеювання літералів на етапі компіляції) та генерацію ефективного байт-коду для змінних (StringBuilder у Java 8, invoke...
🟢 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
// Генерує оптимальний код для даної сигнатури
}
Стратегії: | Стратегія | Механізм | Коли обирається | | ———– | ——————————— | ————————— | | 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 vs StringBuffer]]
- [[19. Що таке compact strings в Java 9+]]