Коли потрібно використовувати intern()?
Метод intern() додає рядок у String Pool і повертає посилання на нього з пулу.
🟢 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:
- Якщо такий рядок вже є — повертає посилання з пулу
- Якщо ні — додає поточний рядок у пул і повертає посилання
Практичне застосування
// Завантаження даних з БД — багато повторюваних значень
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 посилань на них
Типові помилки
-
Помилка: Виклик
intern()для кожного рядка без аналізу Рішення: Використовуйте лише для довгоживучих даних з дублікатами -
Помилка: Очікування миттєвого результату Рішення:
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() виконує:
- Обчислення хешу рядка
- Пошук у хеш-таблиці StringTable
- Якщо знайдено — повернення посилання
- Якщо ні — вставка в таблицю (з можливим resize)
Архітектурні Trade-offs
Плюси intern():
- Економія RAM: при співвідношенні дублікатів 1000:1 економія >99%
- Менше об’єктів → рідше Full GC
- Швидке порівняння через
==(після інтернування)
Мінуси intern():
- CPU overhead: кожен виклик — хешування + lookup у глобальній таблиці
- Contention: StringTable — глобальна структура даних з блокуванням
- Risk OOM: при мільйонах унікальних рядків пул може заповнити Heap
- StringTableSize: якщо таблиця мала — колізії → деградація до O(n)
Edge Cases
-
Багатопотоковий contention: При паралельному виклику
intern()із сотень потоків виникає конкуренція за блокування StringTable. - String Table Size: За замовчуванням 60013 (Java 8+). Якщо плануєте 1M+ унікальних рядків:
-XX:StringTableSize=1000003 - Young Gen рядки:
intern()для короткоживучих рядків контрпродуктивний — вони і так помруть при наступній Minor GC.
Продуктивність
intern()без колізій: ~50-100nsintern()при 1M записів з правильним StringTableSize: ~200-500nsintern()при 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]]