Когда стоит использовать 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 (в какой области памяти)]]