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

Где хранится String Pool (в какой области памяти)

Пул строк (String Pool или String Constant Pool) — это специальный механизм виртуальной машины Java, предназначенный для оптимизации использования памяти за счет кэширования и п...


🟢 Junior Level

Пул строк (String Pool или String Constant Pool) — это специальный механизм виртуальной машины Java, предназначенный для оптимизации использования памяти за счет кэширования и переиспользования уникальных строковых литералов.

Расположение пула строк в памяти кардинально менялось в ходе эволюции Java:

  • В Java 6 и более ранних версиях: String Pool располагался в PermGen (Permanent Generation) — специальной фиксированной области кучи, предназначенной для метаданных классов. Переполнение этой области часто приводило к фатальной ошибке java.lang.OutOfMemoryError: PermGen space.
  • Начиная с Java 7 (точнее, с Java 7u6) и во всех современных версиях (Java 8, 11, 17, 21+): String Pool перенесен и располагается непосредственно в основной куче (Java Heap).

Суть в 30 секундах

Главное, что нужно помнить на собеседовании: в современных версиях Java пул строк находится в Java Heap. Он делит то же самое пространство памяти, что и все обычные объекты приложения, и ограничен общим объемом кучи (-Xmx). Сборщик мусора (GC) автоматически удаляет неиспользуемые строки из пула, если на них больше нет активных ссылок в программе.

⚠️ Критическое заблуждение: Многие разработчики ошибочно полагают, что в Java 8 пул строк переехал в Metaspace. Это не так: в Metaspace переехали только метаданные классов, а String Pool остался в Java Heap.


🟡 Middle Level

Архитектурное разделение: объекты в Heap против индекса в Native Memory

При детальном рассмотрении внутренностей JVM под понятием «String Pool» скрываются две разные сущности:

  1. Сами объекты строк (java.lang.String и их массивы byte[] value): Они аллоцируются в Java Heap (как правило, в Old Generation при длительном хранении или непосредственно во время интернирования).
  2. Индексная хеш-таблица (StringTable в исходниках HotSpot C++): Это служебная структура данных (хеш-таблица адресов), которая размещается в Native Memory (C-Heap) процесса JVM.
┌─────────────────────────────────────────────────────────────┐
│                      JVM Memory                             │
│                                                             │
│   ┌─────────────────────────────────────────────────────┐   │
│   │                     Java Heap                       │   │
│   │                                                     │   │
│   │   [Обычные объекты: User, Order, byte[]]            │   │
│   │                                                     │   │
│   │   [Строковые объекты String Pool: "hello", "admin"] │   │
│   │         ▲                     ▲                     │   │
│   └─────────┼─────────────────────┼─────────────────────┘   │
│             │                     │                         │
│             │ указатели (oop)     │                         │
│   ┌─────────┴─────────────────────┴─────────────────────┐   │
│   │         Native Memory (C-Heap / Process Memory)     │   │
│   │                                                     │   │
│   │   HotSpot C++ StringTable (ConcurrentHashTable)     │   │
│   │   [Bucket 0] ──► [Bucket 1] ──► [Bucket 2]          │   │
│   └─────────────────────────────────────────────────────┘   │
│                                                             │
│   ┌─────────────────────────────────────────────────────┐   │
│   │                     Metaspace                       │   │
│   │   [Klass metadata, Method bytecode, Constant Pools] │   │
│   └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

Зачем String Pool перенесли из PermGen в Java Heap?

В Java 6 нахождение пула строк в PermGen создавало колоссальные проблемы в продакшене:

  1. Жесткий лимит памяти: PermGen имел фиксированный верхний предел (по умолчанию от 64 до 256 МБ, флаг -XX:MaxPermSize). Любое активное использование метода String.intern() быстро приводило к OutOfMemoryError: PermGen space и аварийному падению JVM.
  2. Неэффективная сборка мусора: Сборщик мусора в PermGen вызывался крайне редко (только во время Full GC), а алгоритм отслеживания ссылок для интернированных строк был неэффективен.
  3. Безопасность в Heap: Перенос в общую кучу снял жесткий потолок памяти — теперь пул ограничен только параметром -Xmx (гигабайты памяти), а сборщик мусора очищает неиспользуемые строки пула на регулярной основе вместе с обычными объектами.

Сравнительная таблица по версиям Java

| Характеристика | Java 6 и ниже | Java 7 (начиная с 7u6) | Java 8 – 21+ | | :— | :— | :— | :— | | Область памяти объектов пула | PermGen | Java Heap | Java Heap | | Ограничение памяти | -XX:MaxPermSize (64–256 MB) | -Xmx (общая куча) | -Xmx (общая куча) | | Тип ошибки при переполнении | OOM: PermGen space | OOM: Java heap space | OOM: Java heap space | | Сборка мусора для пула | Только при Full GC (тяжелая) | Обычные циклы GC (Minor/Major) | Параллельная/конкурентная очистка GC | | Где метаданные классов | PermGen | PermGen | Metaspace (Native Memory) | | Реализация StringTable в C++ | Hashtable с глобальным локом | Hashtable с глобальным локом | ConcurrentHashTable (Java 11+, lock-free) |


🔴 Senior Level

Внутреннее устройство StringTable в HotSpot (C++)

В исходном коде OpenJDK HotSpot пул строк реализован как нативная хеш-таблица.

  • До Java 11: Использовался класс StringTable : public Hashtable. Доступ к таблице синхронизировался глобальным мьютексом StringTable_lock. При интенсивном интернировании в многопоточных приложениях потоки вставали в жесткий contention на этом замке.
  • Java 11+ (JEP 341: Concurrent StringTable): Таблица была полностью переписана на ConcurrentHashTable (lock-free структура на базе алгоритмов без ожидания):
    • Потоки могут параллельно искать и вставлять строки без глобальных блокировок.
    • Таблица умеет динамически увеличивать число корзин (resizing) при превышении порога заполненности (Load Factor).

Как Garbage Collector очищает String Pool (Слабые ссылки и OopStorage)

Строки, помещенные в String Pool, не остаются там навечно (если они не зашиты в пуле констант загруженного класса).

  1. Указатели из StringTable на Java-объекты обрабатываются сборщиком мусора как слабые корни (Weak Roots).
  2. В современных версиях HotSpot для этого используется механизм OopStorage (Weak Processor).
  3. Во время фазы маркировки (Marking Phase) сборщик мусора обходит граф живых объектов от Strong Roots (стеки потоков, JNI handles, статические поля).
  4. Если на объект String в пуле нет ни одной сильной ссылки из приложения, сборщик мусора помечает его как кандидата на удаление.
  5. Во время фазы зачистки (Cleaning / Unlinking) JVM вызывает нативный метод StringTable::unlink(), удаляя запись из хеш-таблицы, после чего память объекта String и его массива byte[] освобождается сборщиком мусора как обычный мусор.

Постоянный пул класса (Runtime Constant Pool в Metaspace) против String Pool

Важно различать Run-Time Constant Pool конкретного класса и глобальный String Pool:

  • Constant Pool класса: Находится в памяти Metaspace внутри структуры C++ InstanceKlass. Для каждого строкового литерала в байткоде записан тег CONSTANT_String_info с индексом в таблицу символов CONSTANT_Utf8_info.
  • Разрешение литерала (Resolution): При первом выполнении инструкции байткода ldc (Load Constant) HotSpot берет UTF-8 символы из Metaspace, обращается к глобальному StringTable в Native Memory и находит или создает экземпляр java.lang.String в Java Heap. Ссылка на этот объект кэшируется в Constant Pool Cache класса в Metaspace.
Metaspace: Class Constant Pool Cache [Index #5] ──указатель oop──► Java Heap: String ("test")
                                                                         ▲
Native Memory: StringTable [Bucket Hash] ──weak handle───────────────────┘

4 Tricky Questions

1. Где физически хранятся строковые объекты пула, а где — сама хеш-таблица StringTable в Java 17/21?

Ответ: Здесь происходит разделение памяти:

  • Сами объекты java.lang.String и их внутренние массивы byte[] value физически размещаются в Java Heap (обычно переходя в Old Generation при длительной жизни).
  • Служебная индексная структура — хеш-таблица StringTable (массив указателей на корзины, нативные узлы связных списков и структуры ConcurrentHashTable) — размещается в Native Memory (C-Heap) процесса операционной системы вне Java Heap.

2. Может ли сборщик мусора (GC) удалить строку из String Pool, если мы вызвали "dynamicString".intern()?

Ответ: Да, может. Запись в StringTable удерживает объект по слабой ссылке (weak reference / weak root). Если после вызова String s = new String("data").intern() ссылка s вышла из области видимости (переменная метода завершила работу, объект обнулился), и больше ни один живой объект в Heap не ссылается на этот экземпляр строки, сборщик мусора во время ближайшего цикла GC (или фазы Concurrent Weak Root Processing) удалит запись из StringTable и освободит память в куче. Строка не будет собрана только в том случае, если она жестко зашита в пуле констант класса (пока жив ClassLoader этого класса) либо на нее есть сильная ссылка (Strong Reference) в живых объектах.

3. Почему многие разработчики уверены, что в Java 8 String Pool перенесли в Metaspace, и почему это грубая ошибка?

Ответ: В Java 8 была полностью удалена область памяти PermGen, а вместо нее создана Metaspace в Native Memory для хранения метаданных классов (Klass, Method, Field structures). Из-за этого возникло распространенное заблуждение: «раз PermGen удалили и заменили на Metaspace, значит, все содержимое PermGen переехало в Metaspace». Это ошибка: пул строк был вынесен из PermGen в Java Heap еще в Java 7 (update 6) за несколько лет до выхода Java 8. В Java 8 в Metaspace переехали исключительно метаданные классов, а String Pool остался в Java Heap, где и находится по сей день.

4. Что произойдет, если запустить JVM с параметром -XX:StringTableSize=1009 и интернировать 1 000 000 уникальных строк в Java 8 и в Java 17?

Ответ:

  • В Java 8: Размер StringTable фиксирован (1009 корзин). При добавлении 1 000 000 строк средняя длина цепочки коллизий в каждой корзине составит около 1000 элементов. Поскольку в Java 8 доступ к StringTable защищен глобальным мьютексом StringTable_lock, каждый вызов .intern() превратится в линейный поиск $O(N)$ по связному списку из 1000 элементов под глобальной блокировкой. Производительность приложения упадет в десятки раз (катастрофическая деградация CPU).
  • В Java 17+: Благодаря JEP 341 таблица реализована как ConcurrentHashTable. JVM умеет динамически отслеживать фактор загрузки и автоматически выполнять масштабирование (ресайзинг) количества корзин в фоновом режиме без блокировки читающих потоков. Деградация производительности будет несоизмеримо меньшей.

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

30-секундный ответ

«String Pool в современных версиях Java (начиная с Java 7 и включая Java 8, 11, 17, 21+) располагается в Java Heap. Сами объекты строк и массивы байтов живут в куче и собираются сборщиком мусора, если на них нет сильных ссылок. Служебная хеш-таблица StringTable находится в Native Memory. Распространенный миф о том, что пул находится в Metaspace, ошибочен: в Metaspace хранятся только метаданные классов».

Ключевые тезисы для Senior-интервью

  1. Эволюция: Java 6 (PermGen) $\to$ Java 7u6+ (Java Heap) $\to$ Java 8 (Metaspace появился, но пул остался в Heap).
  2. Память: Объекты String — в Java Heap (лимит -Xmx). Таблица StringTable — в Native Memory (C-Heap).
  3. GC и Weak Roots: Ссылки из StringTable являются слабыми корнями (OopStorage). Неиспользуемые интернированные строки вычищаются через StringTable::unlink().
  4. JEP 341 (Java 11): Замена старой синхронизированной Hashtable на lock-free ConcurrentHashTable с динамическим ресайзингом.
  5. Диагностика: Флаги -XX:StringTableSize, jcmd <pid> VM.stringtable -verbose.

Красные флаги (Чего говорить категорически нельзя)

  • ❌ Утверждать: «String Pool находится в Metaspace» (грубейшая ошибка архитектуры памяти JVM).
  • ❌ Говорить: «Строки из String Pool никогда не удаляются сборщиком мусора» (удаляются, так как удерживаются слабыми ссылками).
  • ❌ Заявлять: «String Pool ограничен отдельным флагом памяти» (пул делит общую память кучи, ограничен -Xmx).

Связанные вопросы