В якій області пам'яті зберігається 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» приховуються дві різні сутності:
- Самі об’єкти рядків (
java.lang.Stringта їхні масивиbyte[] value): Вони виділяються в Java Heap (як правило, потрапляючи в Old Generation при тривалому зберіганні або безпосередньо під час інтернування). - Індексна хеш-таблиця (
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 створювало колосальні проблеми в продакшені:
- Жорсткий ліміт пам’яті: PermGen мав фіксовану верхню межу (за замовчуванням від 64 до 256 МБ, прапорець
-XX:MaxPermSize). Будь-яке активне використання методуString.intern()швидко призводило доOutOfMemoryError: PermGen spaceта аварійного завершення JVM. - Неефективне збирання сміття: Збирач сміття в PermGen викликався вкрай рідко (лише під час Full GC), а алгоритм відстеження посилань для інтернованих рядків був неоптимальним.
- Безпека в 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, не залишаються там назавжди (якщо вони не зашиті в пулі констант завантаженого класу).
- Покажчики зі
StringTableна Java-об’єкти обробляються збирачем сміття як слабкі корені (Weak Roots). - У сучасних версіях HotSpot для цього використовується механізм
OopStorage(Weak Processor). - Під час фази маркування (Marking Phase) збирач сміття обходить граф живих об’єктів від Strong Roots (стеки потоків, JNI handles, статичні поля).
- Якщо на об’єкт
Stringу пулі немає жодного сильного посилання із застосунку, збирач сміття позначає його як кандидата на видалення. - Під час фази зачистки (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).
Через це виникло хибне уявлення: «якщо 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-інтерв’ю
- Еволюція: Java 6 (PermGen) $\to$ Java 7u6+ (Java Heap) $\to$ Java 8 (з’явився Metaspace, але пул залишився в Heap).
- Розподіл пам’яті: Об’єкти
String— у Java Heap (ліміт-Xmx). ТаблицяStringTable— у Native Memory (C-Heap). - GC та Weak Roots: Посилання зі
StringTableє слабкими коренями (OopStorage). Невикористовувані інтерновані рядки видаляються черезStringTable::unlink(). - JEP 341 (Java 11): Заміна старої синхронізованої
Hashtableна lock-freeConcurrentHashTableз динамічним розширенням. - Діагностика: Прапорці
-XX:StringTableSize, утилітаjcmd <pid> VM.stringtable -verbose.
Червоні прапорці (Чого говорити категорично не можна)
- ❌ Стверджувати: «String Pool розташований у Metaspace» (груба помилка розуміння архітектури пам’яті JVM).
- ❌ Говорити: «Рядки зі String Pool ніколи не видаляються збирачем сміття» (видаляються, оскільки утримуються слабкими посиланнями).
- ❌ Заявляти: «String Pool обмежений окремим прапорцем пам’яті» (пул ділить загальну пам’ять купи, обмежений
-Xmx).