Как работает String Pool
Поскольку строки в Java иммутабельны (неизменяемы), хранить в памяти тысячи одинаковых строк вида "USD", "ACTIVE" или "SUCCESS" бессмысленно и расточительно.
🟢 Junior Level
String Pool (Пул строк) — это специальная область хранения в памяти JVM, предназначенная для оптимизации использования оперативной памяти за счет повторного использования (дедупликации) неизменяемых строковых объектов.
Суть в 30 секундах
Поскольку строки в Java иммутабельны (неизменяемы), хранить в памяти тысячи одинаковых строк вида "USD", "ACTIVE" или "SUCCESS" бессмысленно и расточительно.
- При объявлении строки через строковый литерал (
String s = "Hello";) JVM сначала проверяет, есть ли уже строка с таким содержанием в String Pool. - Если строка уже есть в пуле — JVM не создает новый объект, а возвращает ссылку на уже существующий экземпляр.
- Если строки в пуле нет — она создается в куче (Heap), помещается в пул, и возвращается ссылка на нее.
// Пример работы пула строк:
String s1 = "Java";
String s2 = "Java";
// s1 и s2 ссылаются на ОДИН И ТОТ ЖЕ физический объект в памяти!
System.out.println(s1 == s2); // true (сравнение ссылок!)
System.out.println(s1.equals(s2)); // true (сравнение содержимого)
// Создание через оператор new ОБХОДИТ автоматическое использование пула:
String s3 = new String("Java");
System.out.println(s1 == s3); // false (разные объекты в Heap!)
System.out.println(s1.equals(s3)); // true
Зачем нужен String Pool
В среднестатистическом корпоративном Java-приложении строки занимают от 25% до 40% всей оперативной памяти. String Pool позволяет многократно сократить footprint памяти, предотвращая создание сотен тысяч дублирующихся объектов.
🟡 Middle Level
Как устроен String Pool внутри HotSpot JVM
На уровне виртуальной машины (HotSpot JVM) String Pool реализован на языке C++ в виде нативной хеш-таблицы — StringTable.
- Ключами и значениями в таблице являются слабые указатели (
oop, ordinary object pointer) на Java-объектыjava.lang.String, расположенные в куче (Java Heap). StringTableиспользует алгоритм хеширования, основанный на предварительно вычисленномhashстроки.
StringTable (C++ HashMap в HotSpot)
┌─────────────────────────────────┐
bucket[0] ────> │ [hash: 2301548 | ptr: 0x7FA01] │ ───┐
├─────────────────────────────────┤ │
bucket[1] ────> │ NULL │ │
├─────────────────────────────────┤ │
bucket[2] ────> │ [hash: 7120391 | ptr: 0x7FB20] │ │
└─────────────────────────────────┘ │
│
Java Heap (Куча JVM) │
┌─────────────────────────────────┐ │
0x7FA01 ──> │ String Object: "Java" │ <──┘
│ value: byte[] {'J','a','v','a'} │
└─────────────────────────────────┘
Два способа попадания строки в пул
- Автоматически во время компиляции и загрузки класса (Literals):
Все строковые литералы, константы и константные выражения (
"a" + "b") компиляторjavacзаписывает в таблицуConstant_Utf8_infoв секциюConstant Poolвнутри.class-файла. При загрузке и компоновке класса JVM автоматически разрешает эти литералы и заносит ссылки на них вStringTable. - Вручную во время выполнения через метод
String.intern(): Методpublic native String intern()выполняет поиск текущей строки вStringTable:- Если строка с таким содержимым уже есть в пуле — метод возвращает ссылку из пула.
- Если строки в пуле нет — строка, на которой был вызван
intern(), регистрируется вStringTable, и возвращается ссылка на этот же объект.
String dynamicString = new StringBuilder("Ja").append("va").toString(); // Объект в куче вне пула
String internedString = dynamicString.intern(); // Помещаем в пул или берем существующий
String literalString = "Java"; // Взято из пула
System.out.println(internedString == literalString); // true!
🔴 Senior Level
Эволюция расположения String Pool в архитектуре памяти HotSpot
| Версия Java | Область памяти | Проблемы и особенности |
|---|---|---|
| Java 6 и ранее | PermGen (Permanent Generation) | Фиксированный размер (-XX:MaxPermSize). GC в PermGen работал крайне редко и неэффективно. Массовый вызов intern() быстро приводил к падению JVM с ошибкой java.lang.OutOfMemoryError: PermGen space. |
| Java 7 | Java Heap (Основная куча) | String Pool был перенесен в общую кучу (Young/Old Generation). Объекты из пула стали управляться стандартными GC и подчиняться квоте -Xmx. |
| Java 8+ … Java 21+ | Java Heap (НЕ Metaspace!) | Распространенный миф гласит, что с удалением PermGen пул ушел в Metaspace. Это грубейшая ошибка! Метаданные классов ушли в нативную память Native Memory (Metaspace), а строковый пул остался в обычной куче Java Heap. |
Эволюция внутренней структуры StringTable: от Java 7 до Java 11+
До Java 11 нативная таблица StringTable представляла собой хеш-таблицу с фиксированным количеством корзин (fixed-bucket array) и цепочками коллизий в виде связных списков:
- Размер пула задавался флагом
-XX:StringTableSize=N(по умолчанию 1009 в Java 6, 60013 в Java 7u40+ и Java 8). - Главный дефект до Java 11: Таблица не умела автоматически расширяться (отсутствовал rehashing/resize). Если приложение интернировало 5 000 000 уникальных строк при размере таблицы в 60 013 корзин, в каждом бакете образовывалась цепочка длиной ~80 элементов. Сложность поиска деградировала из $O(1)$ в $O(N)$, вызывая катастрофический скачок загрузки CPU!
Революция в Java 11 (JEP 341):
В HotSpot JVM нативная таблица StringTable была полностью переписана на основе ConcurrentHashTable:
- Поддерживает динамический параллельный ресайз (dynamic resizing) при росте load factor.
- Поддерживает масштабируемый конкурентный доступ без глобальных спинлоков (
lock-free lookup). - Интегрирована с механизмом безопасных точек (Safepoints) и фонового удаления неиспользуемых строк при сборке мусора.
Сборка мусора и жизненный цикл строк в пуле
Строки в String Pool НЕ являются бессмертными (начиная с Java 7+):
- Если строка была помещена в пул через
intern(), и в приложении больше не осталось ни одной сильной ссылки (Strong Reference) из GC Roots на этот объект, Garbage Collector имеет полное право удалить этот объект из кучи и очистить запись вStringTable. - Исключение: литералы, привязанные к классам, загруженным системным загрузчиком (
Bootstrap/Application ClassLoader). Они живут до тех пор, пока жив сам класс (то есть фактически все время работы JVM).
Альтернатива intern(): String Deduplication в G1 GC
В высоконагруженных системах массовый вызов s.intern() часто приводит к деградации CPU из-за накладных расходов на расчет хешей и синхронизацию.
Начиная с Java 8u20, в Garbage Collector G1 (а также в Shenandoah и ZGC) появился прозрачный механизм:
-XX:+UseStringDeduplication -XX:StringDeduplicationAgeThreshold=3
- Он работает в фоне во время фаз сборки мусора (Concurrent Marking).
- Если GC видит две строки с одинаковым содержимым, он не объединяет сами объекты
String, но заменяет ссылку на внутренний массивbyte[] valueу второй строки ссылкой на массив первой строки. - Дублирующий массив байт освобождается. Код приложения не меняется,
StringTableне засоряется, а экономия памяти составляет 10–25% в реальном проде!
4 Tricky Questions
1. Может ли вызов String.intern() привести к OutOfMemoryError, и в какой именно области памяти это произойдет на Java 17/21?
Ответ:
Да, может. Начиная с Java 7 и во всех современных версиях (Java 17, 21+) возникнет java.lang.OutOfMemoryError: Java heap space.
Причина: Пул строк физически расположен в обычной куче (Java Heap). Если в бесконечном цикле генерировать уникальные строки (UUID.randomUUID().toString().intern()) и сохранять ссылки на них в долгоживущей коллекции, куча JVM переполнится, сборщик мусора не сможет их удалить, и выбросится ошибка нехватки памяти кучи. (В старых версиях Java 6 и ранее выбросилась бы ошибка OutOfMemoryError: PermGen space).
2. Чему будет равен результат выражения s1 == s2 в следующем коде, и почему:
String s1 = new String("hello").intern();
String s2 = "hello";
Ответ:
Выражение вернет true.
Разбор механики:
- При загрузке класса литерал
"hello"помещается в String Pool. - Инструкция
new String("hello")создает новый объект в куче, содержащий копию символов из пула. - Вызов метода
.intern()на этом новом объекте ищет строку"hello"вStringTable, находит там ранее зарегистрированный литерал и возвращает ссылку на него. Переменнаяs1получает ссылку на строку из пула. - Выражение
String s2 = "hello"берет ссылку на ту же самую строку из пула. - Поскольку обе переменные указывают на один и тот же канонический объект в пуле, сравнение ссылок
==даетtrue.
3. Удаляются ли строки из String Pool сборщиком мусора, если на них больше нет ссылок?
Ответ:
Да, удаляются!
Начиная с Java 7, строковый пул находится в Java Heap. Записи в нативной StringTable хранятся в виде слабых ссылок (Weak References / OopStorage). Если строка была создана динамически во время выполнения и помещена в пул через str.intern(), но в приложении не осталось «живых» ссылок на этот объект из стеков потоков или статических переменных, сборщик мусора (GC) во время очередной фазы очистит этот объект из кучи и удалит соответствующую запись из StringTable.
4. В чем ключевая разница между оптимизацией памяти через String.intern() и JVM-флагом -XX:+UseStringDeduplication?
Ответ:
String.intern():- Работает синхронно в коде приложения.
- Объединяет сами экземпляры
String(делает ссылки идентичными,s1 == s2). - Тратит процессорное время на поиск в нативной хеш-таблице
StringTableпри каждом вызове. - Может создавать contention в многопоточном коде.
-XX:+UseStringDeduplication(в G1/ZGC/Shenandoah):- Работает полностью прозрачно и асинхронно во время фоновой сборки мусора (GC thread).
- НЕ объединяет сами объекты
String(ссылки остаются разными:s1 != s2, ноs1.equals(s2) == true). - Объединяет внутренние массивы байт
byte[] valueу строк, переживших порог выживания (age threshold), освобождая дублирующие массивы из кучи без нагрузки на CPU приложения.
🎯 Шпаргалка для интервью
30-секундный ответ
«String Pool — это область в Java Heap, реализованная как нативная хеш-таблица
StringTableв коде HotSpot JVM. Она предназначена для экономии памяти путем устранения дубликатов строк. Строковые литералы попадают в пул автоматически при загрузке классов, а динамические строки можно поместить вручную черезString.intern(). Начиная с Java 7, пул перенесен из PermGen в кучу и очищается GC при отсутствии сильных ссылок. В современных JVM (Java 11+)StringTableреализован на базеConcurrentHashTableс динамическим масштабированием».
Чек-лист ключевых понятий
- Где хранится: В Java Heap (начиная с Java 7 и во всех современных LTS 11/17/21). Не в Metaspace!
- Как работает
intern(): Возвращает ссылку из пула, если строка уже есть; добавляет строку в пул, если её там не было. - Очистка GC: Строки из пула собираются сборщиком мусора, если на них нет сильных ссылок.
- Реализация в HotSpot:
StringTable(C++ConcurrentHashTableс lock-free чтением в Java 11+). - G1 Deduplication:
-XX:+UseStringDeduplicationдедуплицирует массивыbyte[] valueв фоне без оверхеда наintern().
Красные флаги (чего ни в коем случае нельзя говорить)
- ❌ «String Pool хранится в Metaspace» (Грубейшая ошибка: Metaspace содержит метаданные классов, а String Pool лежит в Java Heap).
- ❌ «Строки из String Pool никогда не удаляются сборщиком мусора» (Динамически интернированные строки без сильных ссылок собираются GC).
- ❌ «String.intern() выполняется мгновенно и не тратит CPU» (Это нативный поиск в хеш-таблице с вычислением хеша, создающий CPU overhead при миллионах строк).