Вопрос 12 · Раздел 12

Может ли String Pool вызвать OutOfMemoryError?

Каждая уникальная строка в пуле — это объект (~48 байт) + запись в хеш-таблице (~32 байта). 100 миллионов уникальных строк = ~8GB только на пул.

Версии по языкам: English Russian Ukrainian

🟢 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); // Растёт бесконечно
}

Как предотвратить

  1. Мониторинг: jcmd <pid> VM.stringtable -verbose
  2. Тюнинг: -XX:StringTableSize=1000003
  3. Альтернатива: -XX:+UseStringDeduplication (G1 GC)
  4. Свой кэш: ConcurrentHashMap<String, String> с eviction

Типичные ошибки

  1. Ошибка: Думать, что GC автоматически почистит пул Решение: StringTable — это нативная хеш-таблица JVM, которая хранит strong references на объекты String. Пока запись в таблице — объект не eligible для GC.

  2. Ошибка: 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

  1. StringTableSize по умолчанию (60013): Если вы интернируете 1M+ уникальных строк, средняя длина цепочки = 1M / 60013 ≈ 16. Худший случай — MUCH больше.

  2. GC и String Pool cleaning: Начиная с Java 7u40, JVM удаляет unreachable записи из StringTable во время Full GC. Но это работает только если на строки нет strong references.

  3. 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]]