Может ли String Pool вызвать OutOfMemoryError?
Каждая уникальная строка в пуле — это объект (~48 байт) + запись в хеш-таблице (~32 байта). 100 миллионов уникальных строк = ~8GB только на пул.
🟢 Junior Level
Да, может. Но тип ошибки зависит от версии Java.
Каждая уникальная строка в пуле — это объект (~48 байт) + запись в хеш-таблице (~32 байта). 100 миллионов уникальных строк = ~8GB только на пул.
Java 6 и раньше:
java.lang.OutOfMemoryError: PermGen space
Пул строк был в области PermGen с фиксированным размером. Много intern() → память закончилась → падение.
Java 7+:
java.lang.OutOfMemoryError: Java heap space
Пул переехал в основную кучу. Если интернировать миллионы уникальных строк — они заполнят всю кучу.
Пример:
// Опасный код — может вызвать OOM
List<String> list = new ArrayList<>();
for (int i = 0; i < 100_000_000; i++) {
list.add(String.valueOf(i).intern()); // Каждая строка уникальна!
}
Как избежать: Не используйте intern() для уникальных строк (UUID, хеши, ID). Используйте только для строк с дубликатами.
🟡 Middle Level
Когда возникает OOM
Сценарий 1: Массовое intern() уникальных строк
// Каждая строка уникальна — пул растёт бесконтрольно
for (User user : users) {
String email = user.getEmail().intern(); // UUID/email — все разные
}
Сценарий 2: Утечка через strong references
// Строки в пуле + ссылки в коллекции = никогда не будут собраны GC
Set<String> cache = new HashSet<>();
while (true) {
String data = readFromNetwork().intern();
cache.add(data); // Растёт бесконечно
}
Как предотвратить
- Мониторинг:
jcmd <pid> VM.stringtable -verbose - Тюнинг:
-XX:StringTableSize=1000003 - Альтернатива:
-XX:+UseStringDeduplication(G1 GC) - Свой кэш:
ConcurrentHashMap<String, String>с eviction
Типичные ошибки
-
Ошибка: Думать, что GC автоматически почистит пул Решение: StringTable — это нативная хеш-таблица JVM, которая хранит strong references на объекты String. Пока запись в таблице — объект не eligible для GC.
-
Ошибка:
intern()для каждой строки из БД Решение: Только для полей с высокой степенью дублирования
🔴 Senior Level
Internal Implementation
StringTable — нативная хеш-таблица:
oop StringTable::intern(Symbol* string, TRAPS) {
unsigned int hashValue = hash_string(string);
int index = the_table()->hash_to_index(hashValue);
oop found_string = the_table()->lookup(index, string, hashValue);
// Found
if (found_string != NULL) return found_string;
// Not found — создать новую запись в StringTable
Handle string_object = java_lang_String::create_from_symbol(string, CHECK_NULL);
the_table()->basic_add(index, string_object, string, hashValue, CHECK_NULL);
return string_object();
}
Каждая запись в StringTable — strong reference. GC не удалит String, пока запись в таблице.
Два типа OOM
Type 1: StringTable overflow (hash collisions)
- При
StringTableSize< количество строк → длинные цепочки коллизий intern()деградирует до O(n)- Приложение “зависает” — CPU 100% на поиск в таблице
- Может проявиться как
GC Overhead Limit Exceeded
Type 2: Heap exhaustion
- Миллионы уникальных интернированных строк заполняют Heap
OOM: Java heap space- Происходит когда пул конкурирует за память с бизнес-объектами
Архитектурные Trade-offs
String Pool и GC:
- Young Gen: новые intern() → Eden → быстро умирают (если нет ссылок)
- Old Gen: долгоживущие intern() → Old Gen → Full GC扫描ит их все
- G1 GC: StringTable scan добавляет к evacuation pause
Contention:
StringTable— глобальная структура с блокировкой- При параллельном
intern()из сотен потоков → contention - Может стать bottleneck в highload системах
Edge Cases
-
StringTableSize по умолчанию (60013): Если вы интернируете 1M+ уникальных строк, средняя длина цепочки = 1M / 60013 ≈ 16. Худший случай — MUCH больше.
-
GC и String Pool cleaning: Начиная с Java 7u40, JVM удаляет unreachable записи из StringTable во время Full GC. Но это работает только если на строки нет strong references.
-
ZGC/Shenandoah: Эти GC используют concurrent marking. StringTable scan происходит concurrently, но overhead всё равно есть.
Производительность
| Метрика | Значение | | ———————————- | ———————————————— | | StringTableSize default | 60013 | | Max safe entries (default size) | ~100K | | intern() без коллизий | ~50-100ns | | intern() с коллизиями (1M entries) | 10-50μs | | Memory per entry | ~48 bytes (String) + ~32 bytes (Hashtable entry) |
Production Experience
Сценарий: ETL pipeline — загрузка 50M записей из CSV:
- Поле
category— 500 уникальных значений →intern()сэкономил 99.9% памяти - Поле
id— 50M уникальных значений →intern()вызвал OOM через 20 минут - Fix:
intern()только дляcategory, дляid— обычный String - Результат: стабильная работа, heap usage снизился с 8GB до 3GB
Правило: если количество уникальных значений поля < 1% от общего количества записей —
intern()имеет смысл. Если > 50% — вреден.
Сценарий 2: API gateway — 100K RPS:
- Каждый запрос:
intern()для header names (Content-Type,Authorization) - StringTable вырос до 500K entries
- Без увеличения StringTableSize: p99 latency вырос с 5ms до 50ms
- Fix:
-XX:StringTableSize=1000003→ p99 вернулся к 5ms
Monitoring
# Статистика StringTable
jcmd <pid> VM.stringtable -verbose
# Вывод:
# Number of buckets : 60013
# Number of entries : 500234
# Maximum bucket size : 87 ← если > 10, проблема!
# GC логи
java -Xlog:gc*:file=gc.log:time,level,tags ...
# Heap histogram
jmap -histo:live <pid> | head -20
Best Practices для Highload
- Никогда не интернируйте уникальные строки (UUID, ID, хеши, timestamps)
- Увеличьте
-XX:StringTableSizeпри ожидаемом > 100K уникальных строк - Мониторьте
Maximum bucket sizeчерезjcmd - Рассмотрите
ConcurrentHashMap<String, String>с size limit + LRU eviction - Для автоматической экономии:
-XX:+UseStringDeduplication(G1 GC)
🎯 Шпаргалка для интервью
Обязательно знать:
- String Pool может вызвать OOM: в Java 6 —
PermGen space, в Java 7+ —Java heap space - Массовый
intern()уникальных строк (UUID, email, ID) — главная причина OOM - StringTable хранит strong references — GC не удалит строки, пока записи в таблице
- Два типа проблем: переполнение хеш-таблицы (коллизии) и исчерпание кучи
Maximum bucket size > 10— признак проблемы, нужно увеличитьStringTableSize- Правило:
intern()имеет смысл если уникальных значений < 1% от общего количества записей
Частые уточняющие вопросы:
- Какие строки НЕ стоит интернировать? — Уникальные: UUID, ID, email, хеши, timestamps.
- Как предотвратить OOM от String Pool? — Увеличить
StringTableSize, не интернировать уникальные строки, мониторить черезjcmd VM.stringtable. - Что такое contention на StringTable? —
StringTable— глобальная структура с блокировкой. При параллельномintern()из сотен потоков — bottleneck. - Может ли GC почистить String Pool? — В Java 7+ да, если на строки нет strong references. Но если вы удерживаете ссылки — не соберутся.
Красные флаги (НЕ говорить):
- ❌ “intern() для каждой строки из БД — хорошая идея” — только для полей с дубликатами
- ❌ “String Pool не может вызвать OOM” — может, и это частая проблема
- ❌ “GC автоматически почистит пул” — только если нет strong references
- ❌ “StringTableSize не нужно настраивать” — при > 100K уникальных строк обязательно
Связанные темы:
- [[1. Как работает String Pool]]
- [[3. Когда стоит использовать intern()]]
- [[11. Где хранится String Pool (в какой области памяти)]]
- [[22. Что такое String deduplication в G1 GC]]