Як працює String Pool?
У типовому Java-додатку рядки становлять 25-40% усіх об'єктів. Без пулу кожна копія "Hello" створювала б окремий об'єкт, навіть якщо текст повністю ідентичний.
🟢 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-файлі, що містить усі літерали, імена методів та інші константи класу.
Два шляхи потрапляння рядка у пул:
- Літерали — автоматично при завантаженні класу:
String s = "Hello"; - Метод 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 (один об'єкт у пулі)
Типові помилки
-
Помилка: Використання
new String("literal")без необхідності Рішення: Використовуйте літерали —String s = "literal"; -
Помилка: Порівняння рядків через
==замість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
-
Колізії в StringTable: Якщо кількість рядків » StringTableSize, пошук перетворюється з O(1) на O(n). Перевірте:
jcmd <pid> VM.stringtable -verbose -
String Pool і GC: У Java 7+ рядки з пулу можуть бути видалені GC, якщо на них немає посилань. Але якщо ви утримуєте посилання — вони ніколи не зберуться.
-
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]]