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

Когда стоит использовать intern()?

Метод intern() добавляет строку в String Pool и возвращает ссылку на неё из пула.

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

🟢 Junior Level

Метод intern() добавляет строку в String Pool и возвращает ссылку на неё из пула.

Представьте: вы загружаете 10 000 записей из БД, и в каждой записи одно и то же слово ‘Ukraine’. Без intern() — 10 000 отдельных объектов. С intern() — один объект и 10 000 ссылок на него.

Простой пример:

String s1 = new String("Hello"); // В обычной куче
String s2 = s1.intern();          // Добавлена в пул

String s3 = "Hello";              // Из пула
System.out.println(s2 == s3);     // true — одна и та же строка из пула

Когда использовать: Когда у вас много одинаковых строк и вы хотите сэкономить память. Например, если загруете из базы данных 10 000 записей, и в каждой записи есть поле country = "Ukraine" — вместо 10 000 объектов в памяти будет один объект в пуле.

Когда НЕ использовать: Для короткоживущих строк, которые быстро удаляются. Обычный Garbage Collector справится с ними сам.


🟡 Middle Level

Как это работает

Метод intern() проверяет String Pool:

  1. Если такая строка уже есть — возвращает ссылку из пула
  2. Если нет — добавляет текущую строку в пул и возвращает ссылку

Практическое применение

// Загрузка данных из БД — много повторяющихся значений
while (rs.next()) {
    String city = rs.getString("city").intern();
    String country = rs.getString("country").intern();
    users.add(new User(city, country));
}

Если в базе 1 000 000 записей, но только 100 уникальных городов:

  • Без intern(): 1 000 000 объектов String
  • С intern(): 100 объектов String в пуле + 1 000 000 ссылок на них

Типичные ошибки

  1. Ошибка: Вызов intern() для каждой строки без анализа Решение: Используйте только для долгоживущих данных с дубликатами

  2. Ошибка: Ожидание мгновенного результата Решение: intern() — нативный вызов с overhead, он не бесплатный

Сравнение: intern() vs String Deduplication

| Характеристика | intern() | -XX:+UseStringDeduplication | | ———————- | ———————- | —————————- | | Когда работает | При вызове (синхронно) | Во время GC (асинхронно) | | Что объединяет | Объекты String | Внутренние byte[] массивы | | Требует изменения кода | Да | Нет (только JVM флаг) | | Только G1 GC | Нет | Да |


🔴 Senior Level

Internal Implementation

String.intern() — нативный метод:

JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str))
  if (str == NULL) return NULL;
  oop string = JNIHandles::resolve_non_null(str);
  oop result = StringTable::intern(string, CHECK_NULL);
  return (jstring) JNIHandles::make_local(env, result);
JVM_END

StringTable::intern() выполняет:

  1. Вычисление хеша строки
  2. Поиск в хеш-таблице StringTable
  3. Если найдено — возврат ссылки
  4. Если нет — вставка в таблицу (с возможным resize)

Архитектурные Trade-offs

Плюсы intern():

  • Экономия RAM: при соотношении дубликатов 1000:1 экономия >99%
  • Меньше объектов → реже Full GC
  • Быстрое сравнение через == (после интернирования)

Минусы intern():

  • CPU overhead: каждый вызов — хеширование + lookup в глобальной таблице
  • Contention: StringTable — глобальная структура данных с блокировкой
  • Risk OOM: при миллионах уникальных строк пул может заполнить Heap
  • StringTableSize: если таблица мала — коллизии → деградация до O(n)

Edge Cases

  1. Многопоточный contention: При параллельном вызове intern() из сотен потоков возникает конкуренция за блокировку StringTable.

  2. String Table Size: По умолчанию 60013 (Java 8+). Если планируете 1M+ уникальных строк:
    -XX:StringTableSize=1000003
    
  3. Young Gen строки: intern() для короткоживущих строк контрпродуктивен — они и так умрут при следующей Minor GC.

Производительность

  • intern() без коллизий: ~50-100ns
  • intern() при 1M записей с правильным StringTableSize: ~200-500ns
  • intern() при 1M записей с малым StringTableSize: 10-50μs (коллизии!)

Production Experience

Сценарий: Парсинг 10GB логов, где встречаются 500 уникальных уровней логирования (INFO, WARN, ERROR, DEBUG, TRACE):

  • Без intern(): ~50M объектов String для ключей → 2.4GB
  • С intern(): 500 объектов в пуле → ~50KB
  • Результат: Full GC каждые 30 секунд → каждые 15 минут, latency p99 снизился с 200ms до 15ms // Меньше объектов в Eden → реже заполняется → реже Minor GC → ниже latency.

Обратный сценарий: UUID пользователей — каждая строка уникальна. intern() здесь только тратит CPU и заполняет пул мусором.

Monitoring

# Статистика StringTable
jcmd <pid> VM.stringtable -verbose

# Вывод:
# StringTable statistics:
# Number of buckets       : 60013
# Number of entries       : 1234567
# Number of loaded classes: N/A
# Maximum bucket size     : 42         ← если > 10, увеличьте StringTableSize

Best Practices для Highload

  • Используйте intern() только для долгоживущих строк с высоким коэффициентом дублирования
  • Не интернируйте UUID, хеши, токены — они уникальны
  • Профилируйте: иногда CPU-overhead от intern() дороже, чем лишние MB в Heap
  • Альтернатива: свой кэш ConcurrentHashMap<String, String> — контроль над eviction и размером
  • Для автоматической дедупликации без кода: -XX:+UseStringDeduplication (G1 GC, начиная с Java 8u20)

🎯 Шпаргалка для интервью

Обязательно знать:

  • intern() добавляет строку в String Pool и возвращает ссылку из пула
  • Экономит память при большом количестве дублирующихся строк (1M записей, 100 городов → 100 объектов вместо 1M)
  • intern() — нативный вызов с CPU overhead (~50-100ns без коллизий)
  • Contention: StringTable — глобальная структура с блокировкой, bottleneck при сотнях потоков
  • -XX:StringTableSize=1000003 — увеличьте при 1M+ уникальных строк
  • Не используйте intern() для UUID, хешей, токенов — они все уникальны

Частые уточняющие вопросы:

  • Когда intern() полезен? — При загрузке данных с высокой степенью дублирования: словари, категории, города, статусы.
  • Когда intern() вреден? — Для уникальных строк: UUID, ID, email, хеши. Заполняет пул, тратит CPU, не экономит память.
  • Что быстрее: intern() или свой кэш через ConcurrentHashMap?ConcurrentHashMap даёт контроль над eviction и размером, но intern() — JVM-native, без manual management.
  • Какой overhead у intern()? — ~50-100ns без коллизий. При 1M записей с малым StringTableSize: 10-50μs (коллизии!).

Красные флаги (НЕ говорить):

  • ❌ “intern() для каждой строки — хорошая практика” — только для строк с дубликатами
  • ❌ “intern() бесплатный” — нативный вызов, CPU overhead, contention на StringTable
  • ❌ “intern() ускоряет всё” — экономит память, но замедляет CPU
  • ❌ “intern() — единственная оптимизация строк” — есть -XX:+UseStringDeduplication (автоматическая, без кода)

Связанные темы:

  • [[1. Как работает String Pool]]
  • [[12. Может ли String Pool вызвать OutOfMemoryError]]
  • [[22. Что такое String deduplication в G1 GC]]
  • [[11. Где хранится String Pool (в какой области памяти)]]