Питання 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]]