Що таке компактні рядки в Java 9+?
Кожен символ зберігався у 2 байтах (UTF-16), навіть для простих літер на кшталт "a", "b", "c". Рядок "Hello" займав 10 байт.
🟢 Junior Level
Compact Strings — це оптимізація, що з’явилася в Java 9, яка дозволяє рядкам займати вдвічі менше пам’яті, якщо вони містять лише прості латинські символи (англійські літери, цифри, базова пунктуація).
Як це працювало до Java 9: Кожен символ зберігався у 2 байтах (UTF-16), навіть для простих літер на кшталт “a”, “b”, “c”. Рядок “Hello” займав 10 байт.
Як працює в Java 9+:
- Якщо рядок містить лише «прості» символи (Latin-1, U+0000–U+00FF) → 1 байт на символ
- Якщо є складні символи (кирилиця, ієрогліфи, емодзі) → 2 байти на символ, як раніше
- Java сама вирішує, який формат використовувати — вам не потрібно нічого змінювати в коді
Приклад:
String english = "Hello World"; // 5 байт (було б 10 в Java 8)
String russian = "Привіт Мир"; // 10 байт (UTF-16, як і раніше)
Проста аналогія: Уявіть валізу. Раніше Java завжди клала речі у велику валізу (2 байти), навіть якщо ви берете тільки шкарпетки. Тепер Java дивиться: якщо речей мало — бере маленьку валізу (1 байт), якщо багато — велику (2 байти).
Вам не потрібно нічого змінювати. Це працює повністю прозоро.
🟡 Middle Level
Як це реалізовано всередині
В Java 9 поле char[] value замінено на byte[] value + флаг coder:
public final class String {
@Stable
private final byte[] value; // Раніше було char[]
private final byte coder; // 0 = Latin-1, 1 = UTF-16
}
- coder = 0 (LATIN1): 1 байт на символ, діапазон U+0000–U+00FF
- coder = 1 (UTF16): 2 байти на символ, всі Unicode символи
Автоматичне перемикання
String latin = "Hello"; // coder = LATIN1, 1 byte/char
String cyrillic = "Привіт"; // coder = UTF16, 2 bytes/char
// Конкатенація Latin-1 + Cyrillic → UTF-16 (весь рядок розширюється)
String mixed = latin + cyrillic; // coder = UTF16 для всього рядка
Таблиця типових помилок
| Помилка | Наслідки | Рішення |
|---|---|---|
| Думати, що compact strings — це стиснення (як gzip) | Очікування економії для будь-яких даних | Це просто ефективніший формат зберігання, тільки для Latin-1 |
Очікування, що -XX:+CompactStrings потрібно вмикати |
Плутанина з флагами | Ввімкнено за замовчуванням з Java 9. Флаг -XX:-CompactStrings вимикає |
| Очікування, що substring «знизить» coder | Неефективне використання пам’яті | JVM не «знижує» UTF-16 → Latin-1 автоматично |
Порівняння пам’яті
| Рядок | Java 8 (char[]) |
Java 9+ (byte[]) |
Економія | |
|---|---|---|---|---|
"Hello" |
10 bytes | 5 bytes | 50% | |
"Hello World" |
22 bytes | 11 bytes | 50% | |
"Привіт" |
12 bytes | 12 bytes | 0% | |
"Hello Привіт" |
24 bytes | 24 bytes | 0% | |
"" (порожній) |
~40 bytes | ~40 bytes | 0% | String object (~24) + порожній byte[] (~16) |
Коли НЕ покладатися на compact strings
- Текст кирилицею/китайською/японською — завжди UTF-16, економії немає
- Змішаний текст (Latin + Cyrillic) — весь рядок стає UTF-16
- Substrings від UTF-16 рядків — наслідують UTF-16 coder, навіть якщо підрядок містить тільки ASCII
🔴 Senior Level
Internal Implementation — JEP 254
JEP 254: Compact Strings (Java 9) змінив внутрішнє представлення String:
// Ключові методи з перевіркою coder
public int length() {
return value.length >> coder; // LATIN1: >> 0 = без зсуву; UTF16: >> 1 = /2
}
public char charAt(int index) {
if (isLatin1()) {
return (char)(value[index] & 0xff);
}
return StringUTF16.getChar(value, index);
}
public boolean equals(Object anObject) {
if (this == anObject) return true;
if (anObject instanceof String another) {
if (coder == another.coder) // Спочатку перевіряємо coder!
return isLatin1()
? StringLatin1.equals(value, another.value)
: StringUTF16.equals(value, another.value);
}
}
return false; // Різні coder → завідомо не рівні
}
Кожен метод String тепер перевіряє coder і делегує роботу в StringLatin1 або StringUTF16. Ці делегати використовують intrinsic-методи JVM — JIT компілятор генерує SIMD-оптимізований код для побайтового порівняння.
Trade-offs
Плюси:
- Економія пам’яті: 40–50% для типових Enterprise-додатків (JSON keys, log levels, HTTP headers — все ASCII)
- GC pressure: менше об’єктів у Heap → рідше GC паузи
- Cache locality: компактні дані → більше поміщається в L1/L2 кеш CPU → швидша обробка
- Безкоштовна оптимізація — zero code change
Мінуси:
- Невеликий overhead на перевірку
coderв кожному методі (JIT зазвичай елімінує через constant folding) - Конкатенація Latin-1 + UTF-16 → UTF-16 (розширення всього рядка)
- Довелося переписати всі intrinsic-оптимізації для двох форматів
- Reflection-код, що працював з
char[], зламався
Edge Cases (мінімум 3)
1. Coder mismatch при конкатенації:
String latin = "Hello"; // Latin-1
String cyrillic = "Мир"; // UTF-16
String result = latin + " " + cyrillic; // Весь рядок → UTF-16
// Навіть "Hello " розширюється до UTF-16 в результаті
// Втрата: 6 байт → 12 байт для Latin-1 частини
2. Substring не «знижує» coder:
String mixed = "Hello Мир"; // UTF-16 (містить кирилицю)
String sub = mixed.substring(0, 5); // "Hello" — все ще UTF-16!
// JVM не «знижує» до Latin-1 автоматично
// sub займає 10 байт замість можливих 5
3. Reflection і byte[]:
// В Java 9+ не можна просто так отримати value через рефлексію
// byte[] value замість char[] — зламало старий reflection-код
// Module system (Java 9+) додатково обмежує доступ до internal полів
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true); // Requires --add-opens java.base/java.lang
4. Coder при створенні через конструктор:
// new String(byte[], Charset) визначає coder за вмістом
byte[] ascii = {72, 101, 108, 108, 111}; // "Hello"
String s = new String(ascii, StandardCharsets.UTF_8); // coder = LATIN1
byte[] cyrillicBytes = {(byte)0xD0, (byte)0x9F}; // "П" в UTF-8
String s2 = new String(cyrillicBytes, StandardCharsets.UTF_8); // coder = UTF16
Продуктивність
| Операція | Java 8 (char[]) |
Java 9+ Compact | Improvement |
|---|---|---|---|
| Memory “Hello” | 22 bytes | 11 bytes | -50% |
| Memory “Hello World!” | 34 bytes | 18 bytes | -47% |
charAt() |
~1ns | ~1ns (intrinsic) | Same |
equals() (Latin-1) |
~2ns | ~1.5ns (SIMD on byte[]) |
-25% |
equals() (UTF-16) |
~2ns | ~2ns | Same |
| GC throughput | Baseline | +10–15% | Better |
// Приблизні значення (JMH). Реальні залежать від CPU і JVM.
Пам’ять (64-bit JVM, CompressedOops):
- Latin-1 String: 24 bytes (object header) + 16 + N bytes (
byte[]) ≈ 40 + N bytes - UTF-16 String: 24 bytes + 16 + 2N bytes ≈ 40 + 2N bytes
- Для 1M рядків «Hello»: економія ~5MB у Heap
Thread Safety
String залишається повністю thread-safe. Поле coder — final, масив value — @Stable (JVM-аннотація, що гарантує незмінність після конструювання). Жодних race conditions при читанні з кількох потоків.
Production War Story
Сценарій: Міграція Java 8 → 17 в мікросервісі (4GB Heap, Spring Boot, JSON API).
- До: Heap usage 75% (3GB), Full GC кожні 20 хвилин, p99 latency = 15ms
- Після: Heap usage 55% (2.2GB), Full GC кожні 45 хвилин, p99 latency = 10ms
- JSON keys (
"id","name","type","status") — всі Latin-1 → 50% економії - Без жодного рядка коду — transparent upgrade
Сценарій 2: Highload парсер (1M рядків/сек, логи):
- Рядки: JSON keys — всі Latin-1
- Економія: ~200MB/sec алокацій → ~100MB/sec
- Young GC duration знизилася на 30%
- Проблема: Підрядки від UTF-16 рядків наслідували UTF-16 coder → неочікуване споживання пам’яті для «простих» рядків. Fix: явне створення нових рядків через конструктор.
Monitoring
# Перевірити, що Compact Strings увімкнено
java -XX:+PrintFlagsFinal -version | grep CompactStrings
# bool CompactStrings = true {product}
# JOL — реальний розмір
# String s = "Hello";
# GraphLayout.parseInstance(s).toPrintable()
# Java 9+: value = byte[5] (5 bytes)
# Java 8: value = char[5] (10 bytes)
# GC логи — помітите зниження heap usage
java -Xlog:gc*:file=gc.log ...
Best Practices для Highload
- Compact Strings увімкнено за замовчуванням — нічого робити не потрібно
- Для максимального профіту: зберігайте дані в Latin-1 коли можливо (ASCII keys, enum-значення, статуси)
- Не намагайтеся вручну «знижувати» coder — JIT справляється краще
-XX:-CompactStringsдля вимкнення (але навіщо?)- Профіт найбільш помітний у додатках з великою кількістю рядків: web, JSON parsing, logging, HTTP headers
- При міграції Java 8 → 9+: перевірте reflection-код, що працював з
char[] value - Для ultra-low-latency: компактні рядки покращують cache locality → менше L1/L2 cache miss
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- Compact Strings (JEP 254, Java 9+) — оптимізація: Latin-1 (1 байт/символ) замість UTF-16 (2 байти)
byte[] value+byte coderзамістьchar[] value(Java 8 і раніше)- coder = 0 → Latin-1 (U+0000–U+00FF), coder = 1 → UTF-16
- Ввімкнено за замовчуванням, флаг
-XX:-CompactStringsвимикає - Конкатенація Latin-1 + UTF-16 → результат UTF-16 (весь рядок розширюється)
- Економія: 40-50% пам’яті для типових Enterprise-додатків (JSON keys, HTTP headers — все ASCII)
- Substring від UTF-16 рядка наслідує UTF-16 coder, навіть якщо підрядок — тільки ASCII
Часті уточнюючі запитання:
- Яка економія пам’яті від Compact Strings? — 40-50% для Latin-1 рядків. У типовому web-додатку 70% рядків — Latin-1, загальне зниження Heap на 20-30%.
- Що буде при конкатенації Latin-1 + Cyrillic? — Весь рядок стане UTF-16. Latin-1 частина розшириться до 2 байт/символ.
- Чи знижує
substring()coder з UTF-16 на Latin-1? — Ні. Підрядок від UTF-16 рядка залишається UTF-16, навіть якщо містить тільки ASCII. - Чи потрібно вмикати Compact Strings флагом? — Ні, ввімкнено за замовчуванням з Java 9.
-XX:-CompactStrings— вимикає.
Червоні прапорці (НЕ говорити):
- ❌ “Compact Strings — це стиснення як gzip” — це просто ефективніший формат зберігання
- ❌ “Потрібно вмикати
-XX:+CompactStrings” — вже ввімкнено за замовчуванням - ❌ “Compact Strings працюють для кирилиці” — кирилиця = UTF-16, економії немає
- ❌ “Java автоматично знижує UTF-16 → Latin-1” — не знижує, тільки підвищує
Пов’язані теми:
- [[17. Що таке кодування String]]
- [[20. Як дізнатися, скільки пам’яті займає String]]
- [[13. Що робить метод substring() і як він працював до Java 7]]
- [[22. Що таке дедуплікація рядків в G1 GC]]