Как работает String pool и как это связано с иммутабельностью
Когда в коде объявляется строковый литерал (например, String s = "Java";):
🟢 Junior Level
30-секундный ответ
String Constant Pool (Пул строк) — это специализированная область в оперативной памяти (Heap) виртуальной машины JVM, реализующая паттерн проектирования Flyweight (Приспособленец) для исключения дубликатов строк в памяти.
Когда в коде объявляется строковый литерал (например, String s = "Java";):
- JVM проверяет наличие строки с таким содержимым в пуле.
- Если строка уже существует, JVM возвращает ссылку на существующий объект в куче, не выделяя новую память.
- Если строки в пуле нет, создаётся новый объект, регистрируется в пуле, и ссылка возвращается вызывающему коду.
Почему String Pool физически невозможен без иммутабельности?
Представьте, что класс String был бы мутабельным (как StringBuilder):
String s1 = "admin";
String s2 = "admin"; // s1 и s2 указывают на ОДИН И ТОТ ЖЕ объект в пуле
// Если бы строки были мутабельными:
s1.setCharAt(0, 'u'); // Изменили "admin" на "udmin" через s1
В результате переменная s2 (а также все внутренние подсистемы безопасности JVM, библиотеки авторизации и другие потоки, использовавшие литерал "admin") внезапно получили бы значение "udmin". Это вызвало бы катастрофический каскад ошибок и полный паралич безопасности.
Вывод: Совместное использование одного объекта миллионами независимых потоков и модулей возможно исключительно благодаря тому, что строковый объект гарантированно неизменяем (Immutable).
Наглядный пример
public class StringPoolDemo {
public static void main(String[] args) {
String s1 = "Hello";
String s2 = "Hello"; // Взято из String Pool
String s3 = new String("Hello"); // Принудительное выделение нового объекта в Heap
System.out.println(s1 == s2); // true — один и тот же объект в памяти
System.out.println(s1 == s3); // false — разные ссылки в памяти
System.out.println(s1.equals(s3)); // true — логическое равенство содержимого
}
}
🟡 Middle Level
Байткод: Таблица констант и инструкция ldc
На этапе компиляции исходного кода компилятор javac помещает все литералы в таблицу констант .class-файла (Constant Pool в секции CONSTANT_Utf8_info).
При выполнении метода JVM использует инструкцию ldc (Load Constant):
0: ldc #2 // Загружает ссылку на "Hello" из String Pool
2: astore_1 // Сохраняет в локальную переменную s1
3: new #3 // class java/lang/String (выделяет память в куче)
6: dup
7: ldc #2 // Снова загружает "Hello" из пула для передачи в конструктор
9: invokespecial #4 // Вызов конструктора String(String)
12: astore_3
Метод String.intern()
Метод intern() переносит строку, созданную динамически в рантайме (например, вычитанную из базы данных, файла или REST-запроса), в String Pool:
String dynamicString = new String("custom"); // Создан в куче вне пула
String pooledString = dynamicString.intern(); // Возвращает канонический экземпляр из пула
System.out.println(pooledString == "custom"); // true!
Если строка с таким содержимым уже была в пуле, intern() возвращает ссылку на неё. Если не было — регистрирует текущую строку в пуле.
Эволюция расположения String Pool в памяти HotSpot JVM
| Версия Java | Область памяти | Риски и особенности работы GC |
| :— | :— | :— |
| Java 6 и ранее | PermGen (Permanent Generation) | Фиксированный размер (обычно 64–128 МБ). Интенсивный вызов intern() быстро приводил к OutOfMemoryError: PermGen space. Сборка мусора в PermGen выполнялась редко и неохотно. |
| Java 7+ | Основная куча (Heap Space) | String Pool перемещён в общую кучу. Неиспользуемые интернированные строки могут свободно и эффективно собираться обычным Garbage Collector. |
| Java 9+ | Heap Space (Compact Strings) | Внедрён JEP 254: интернированные строки хранят byte[] вместо char[], экономя до 50% памяти для Latin-1 символов. |
🔴 Senior Level
Внутреннее устройство: Нативная StringTable в C++ коде HotSpot
Внутри JVM HotSpot пул строк представляет собой нативную C++ хеш-таблицу StringTable:
- Исторически это была хеш-таблица с открытой адресацией или связными списками бакетов (Chaining).
- Начиная с Java 13: нативная таблица переписана на Concurrent Hash Table (
ConcurrentHashTableв C++), что устранило глобальные блокировки при параллельном вызовеString.intern()из десятков потоков. - Параметр тюнинга:
-XX:StringTableSize=N(по умолчанию $65536$ бакетов в 64-битных HotSpot Java 11/17/21). - Если приложение бездумно интернирует миллионы уникальных строк (например, UUID транзакций), размер бакетов увеличивается, и вызов
intern()деградирует из $O(1)$ в $O(N)$ по времени, парализуя работу CPU.
Унифицированная дедупликация строк (Java 18+, JDK-8267186)
В современных приложениях ручной вызов intern() считается антипаттерном. Для оптимизации памяти используют флаг -XX:+UseStringDeduplication:
- Начиная с Java 18, функционал дедупликации строк вынесен из G1 в универсальный интерфейс сборщиков мусора и поддерживается в G1, Parallel, Serial и ZGC.
- Как это работает: GC в фоновом режиме находит объекты
Stringс одинаковым хэшем и идентичным байтовым массивомvalue. Ссылкаvalueодного объекта перенаправляется на массив другого, а дублирующий массив байтов освобождается из памяти. - В отличие от
intern(), это происходит асинхронно в фоне, не нагружает потоки приложения и не засоряет нативную таблицуStringTable.
4 Tricky Questions
1. Сколько объектов создаёт инструкция String s = new String("Java")? При каких условиях создаётся 1, 2 или 0 объектов?
Ответ:
- В стандартном сценарии (первое появление литерала в коде): создаётся 2 объекта. Первый объект — строковый литерал
"Java", который загружается в String Pool при загрузке класса. Второй объект — новый экземплярStringв обычной куче, создаваемый операторомnew. - Если строка
"Java"уже присутствовала в пуле строк ранее: создаётся ровно 1 объект (только новый экземпляр в куче черезnew, литерал берётся готовым из пула). - Если JIT-компилятор C2 выполняет Escape Analysis и Dead Code Elimination: если переменная
sне используется и не покидает метод, оптимизатор HotSpot может полностью устранить выделение памяти, создав 0 объектов в куче.
2. Являются ли интернированные строки в String Pool корнями сборки мусора (GC Roots)? Может ли интернированная строка быть удалена GC?
Ответ: Начиная с Java 7, строки в String Pool НЕ являются безусловными GC Roots:
- Сама нативная таблица
StringTableудерживает объектыStringчерез так называемые слабые ссылки или специальные дескрипторы (OopStorage / Weak References). - Если строка попала в пул через динамический вызов
str.intern(), и во всей работающей программе не осталось ни одной живой сильной ссылки (Strong Reference) на этот объект, Garbage Collector имеет полное право удалить эту строку из кучи и очистить соответствующую запись вStringTableво время фазы очистки слабых ссылок. - Строковые литералы из загруженных классов удерживаются через константный пул соответствующего загрузчика классов (
ClassLoader), поэтому они живут столько же, сколько живёт самClassLoader.
3. Чем с точки зрения JVM отличается склейка строк "a" + "b" от склейки s1 + s2?
Ответ:
"a" + "b": является константным выражением времени компиляции (Compile-Time Constant Expression). Компиляторjavacещё на этапе сборки объединяет их в один литерал"ab". В байткод попадает одна инструкцияldc "ab", и в кучу на этапе выполнения ничего лишнего не аллоцируется.s1 + s2: переменные не являются константами времени компиляции. В Java 9+ эта операция компилируется в инструкциюinvokedynamicс методом-бутстрапомStringConcatFactory.makeConcatWithConstants. Во время выполнения вычисляется суммарная длина, выделяется новый массивbyte[]в куче, и возвращается совершенно новый объектString, который не попадает в String Pool автоматически.
4. Если сборщик мусора дедуплицировал массивы двух строк с помощью -XX:+UseStringDeduplication, будет ли выражение s1 == s2 возвращать true?
Ответ:
Нет, выражение s1 == s2 вернёт false!
Дедупликация строк сборщиком мусора работает на уровне внутреннего массива byte[] value:
- Объекты-обёртки
Stringв памяти остаются двумя совершенно разными объектами с разными адресами в куче (s1 != s2). - Единственное, что делает GC, — это перенаправляет внутреннее поле
s2.value = s1.value, освобождая дубликат массива байтов. - Логическое равенство
s1.equals(s2)при этом вернётtrue. Для того чтобыs1 == s2сталоtrue, необходимо явно использовать методs1.intern() == s2.intern().
🎯 Шпаргалка для интервью
Главное о String Pool и иммутабельности
- Паттерн: Flyweight (экономное переиспользование одинаковых строковых литералов).
- Связь с иммутабельностью: критическая. Без неизменяемости изменение одного литерала испортило бы данные во всех потоках приложения.
- Расположение: с Java 7 находится в основной куче (Heap), собирается GC при отсутствии сильных ссылок.
- Внутренности: нативная C++ таблица
StringTable(с Java 13 — lock-free Concurrent Hash Table). - Альтернатива
intern(): флаг-XX:+UseStringDeduplication(Java 18+ поддерживает G1, Parallel, Serial, ZGC).
Литерал vs new String() vs intern()
| Операция | Где выделяется память | Попадает в String Pool? | Сравнение по == с литералом |
| :— | :— | :— | :— |
| String s = "txt"; | В куче (String Pool) | Да (автоматически) | true |
| new String("txt"); | В куче (вне пула) | Нет | false |
| str.intern(); | Возврат из String Pool | Да | true |
Красные флаги на собеседовании (Чего говорить нельзя)
- ❌ «String Pool находится в PermGen» — устарело с Java 7 (пул находится в Heap, PermGen ликвидирован в Java 8).
- ❌ «Интернированные строки никогда не удаляются сборщиком мусора» — неиспользуемые строки удаляются GC из пула.
- ❌ «String Deduplication объединяет сами объекты String, поэтому после этого s1 == s2» — дедуплицируются только внутренние массивы
byte[], сами ссылкиStringостаются разными.