Чи може 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. Найгірший випадок — НАБАГАТО більше.
-
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. Що таке дедуплікація рядків в G1 GC]]