Что такое compact strings в 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 encoding (кодировка)]]
- [[20. Как узнать, сколько памяти занимает String]]
- [[13. Что делает метод substring() и как он работал до Java 7]]
- [[22. Что такое String deduplication в G1 GC]]