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

Что такое compact strings в Java 9+?

Каждый символ хранился в 2 байтах (UTF-16), даже для простых букв вроде "a", "b", "c". Строка "Hello" занимала 10 байт.

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

🟢 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. Поле coderfinal, массив 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]]