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

Как работает String Pool?

В типичном Java-приложении строки составляют 25-40% всех объектов. Без пула каждая копия "Hello" создавала бы отдельный объект, даже если текст полностью идентичен.

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

🟢 Junior Level

String Pool (Пул строк) — это специальный механизм в JVM, который хранит только одну копию каждой уникальной строки. Это помогает экономить память.

В типичном Java-приложении строки составляют 25-40% всех объектов. Без пула каждая копия "Hello" создавала бы отдельный объект, даже если текст полностью идентичен.

Когда вы создаёте строку через литерал, JVM проверяет, есть ли уже такая строка в пуле. Если есть — возвращает ссылку на существующий объект. Если нет — создаёт новую и добавляет в пул.

Пример:

String s1 = "Hello";
String s2 = "Hello";
// s1 и s2 — это один и тот же объект в памяти!
System.out.println(s1 == s2); // true

Зачем это нужно: Если бы каждая строка создавалась как отдельный объект, приложение потребляло бы гораздо больше памяти, особенно если одинаковые строки встречаются часто.

Когда использовать: Используйте литералы строк (String s = "value";) по умолчанию — JVM сама поместит их в пул.


🟡 Middle Level

Как это работает

String Pool реализован как хеш-таблица StringTable внутри JVM. При загрузке класса все строковые литералы из константного пула класса (Constant Pool) автоматически помещаются в String Pool.

Constant Pool — таблица в .class-файле, содержащая все литералы, имена методов и другие константы класса.

Два пути попадания строки в пул:

  1. Литералы — автоматически при загрузке класса: String s = "Hello";
  2. Метод intern() — вручную во время выполнения: String s = new String("Hello").intern();

Практическое применение

String s1 = "Java";           // Литерал → сразу в пуле
String s2 = new String("Java"); // new → новый объект в куче (НЕ в пуле)
String s3 = s2.intern();      // intern() → возвращает ссылку из пула на s1

System.out.println(s1 == s2); // false (разные объекты)
System.out.println(s1 == s3); // true (один объект в пуле)

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

  1. Ошибка: Использование new String("literal") без необходимости Решение: Используйте литералы — String s = "literal";

  2. Ошибка: Сравнение строк через == вместо equals() Решение: Всегда используйте equals() для сравнения содержимого

Сравнение подходов

| Подход | В пуле? | Создаёт объект? | Когда использовать | | ——————— | —————— | ———————- | ——————————————– | | Литерал "Hello" | Да | Только если нет в пуле | 99% случаев | | new String("Hello") | Нет (литерал — да) | Всегда новый | Практически никогда | | s.intern() | Да | Только если нет в пуле | При работе с огромным количеством дубликатов |


🔴 Senior Level

Internal Implementation

String Pool — это нативная хеш-таблица StringTable с открытой адресацией (method chaining). Ключами и значениями являются ссылки на объекты java.lang.String.

// Упрощённая структура из OpenJDK
class StringTable : public RehashableHashtable<oop, mtSymbol> {
  // oop — pointer to Java object
  // Использует hashing через String::hash_code
};

Ключевые параметры JVM:

  • -XX:StringTableSize=N — размер хеш-таблицы (по умолчанию 60013 в Java 8+, ранее 1009)
  • Начиная с JDK 11 (JEP 341), StringTable поддерживает динамическое расширение (resize) при превышении load factor, аналогично HashMap.

Эволюция памяти String Pool

Версия Java Расположение Проблемы
Java 6 и ниже PermGen Фиксированный размер, частые OOM: PermGen space
Java 7+ Java Heap Управляется GC, ограничен только -Xmx
Java 8+ (с Metaspace) Java Heap (не Metaspace!) Всё ещё может вызвать OOM: Java heap space

Распространённое заблуждение: String Pool находится в Metaspace. Это неверно — он остался в Heap.

Архитектурные Trade-offs

Массовое использование intern():

Плюсы:

  • Резкое сокращение потребления RAM при дублирующихся строках
  • Меньше объектов → меньше пауз GC

Минусы:

  • intern() — нативный вызов с вычислением хеша и поиском в таблице
  • При большом количестве строк contention на глобальной StringTable
  • Если StringTableSize мал → длинные цепочки коллизий → деградация O(n)

Edge Cases

  1. Коллизии в StringTable: Если количество строк » StringTableSize, поиск превращается из O(1) в O(n). Проверьте: jcmd <pid> VM.stringtable -verbose

  2. String Pool и GC: В Java 7+ строки из пула могут быть удалены GC, если на них нет ссылок. Но если вы удерживаете ссылки — они никогда не соберутся.

  3. Compact Strings (Java 9+): Не влияют на механизм пула напрямую, но экономят 50% памяти для латинских строк внутри пула.

Производительность

  • Lookup в пустом пуле: ~наносекунды
  • Lookup в пуле с 1M строк (правильный StringTableSize): ~десятки наносекунд
  • Lookup в пуле с 1M строк (малый StringTableSize): микросекунды (коллизии!)

Production Experience

При загрузке миллионов записей из БД (например, 1M пользователей с полем country), где уникальных стран всего 200:

  • Без intern(): 1M объектов String → ~48MB
  • С intern(): 200 объектов в пуле + 1M ссылок → ~5MB
  • Но: overhead на CPU при каждом вызове intern() может быть 10-50%

Monitoring

# Статистика StringTable
jcmd <pid> VM.stringtable -verbose

# Анализ через JOL
System.out.println(GraphLayout.parseInstance(stringTable).toFootprint());

Best Practices для Highload

  • Увеличьте -XX:StringTableSize до простого числа > количества ожидаемых уникальных строк
  • Не используйте intern() для короткоживущих строк (умрут в Young Gen и так)
  • Рассмотрите -XX:+UseStringDeduplication (G1 GC) как прозрачную альтернативу

🎯 Шпаргалка для интервью

Обязательно знать:

  • String Pool — хеш-таблица (StringTable) в JVM, хранит одну копию каждой уникальной строки
  • Литералы автоматически попадают в пул при загрузке класса
  • new String("...") создаёт отдельный объект в куче, НЕ в пуле
  • intern() добавляет строку в пул и возвращает ссылку из пула
  • В Java 7+ String Pool находится в Java Heap (не в PermGen/Metaspace!)
  • -XX:StringTableSize — размер хеш-таблицы (по умолчанию 60013 в Java 8+)
  • При коллизиях в StringTable поиск деградирует из O(1) в O(n)
  • Compact Strings (Java 9+) экономят 50% памяти для Latin-1 строк в пуле

Частые уточняющие вопросы:

  • Где находится String Pool? — В Java Heap (начиная с Java 7). Распространённая ошибка — отвечать Metaspace.
  • Как строка попадает в пул? — Через литералы (автоматически) или вызов intern() (вручную).
  • Можно ли удалить строку из пула? — Да, в Java 7+ GC может собрать строки из пула, если на них нет ссылок.
  • Что будет при new String("Hello").intern()? — Создаётся объект в Heap, затем intern() находит "Hello" в пуле и возвращает ссылку из пула. Объект из Heap становится мусором.

Красные флаги (НЕ говорить):

  • ❌ “String Pool находится в Metaspace” — неверно, он в Heap
  • ❌ “== всегда работает для строк” — работает только для литералов/интернированных строк
  • ❌ “intern() бесплатный” — это нативный вызов с CPU overhead
  • ❌ “String Pool имеет фиксированный размер” — он ограничен только -Xmx в Java 7+

Связанные темы:

  • [[2. В чём разница между созданием String через литерал и через new]]
  • [[3. Когда стоит использовать intern()]]
  • [[11. Где хранится String Pool (в какой области памяти)]]
  • [[12. Может ли String Pool вызвать OutOfMemoryError]]
  • [[22. Что такое String deduplication в G1 GC]]