Питання 1 · Розділ 12

Як працює String Pool?

У типовому Java-додатку рядки становлять 25-40% усіх об'єктів. Без пулу кожна копія "Hello" створювала б окремий об'єкт, навіть якщо текст повністю ідентичний.

Мовні версії: English Russian Ukrainian

🟢 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-файлі, що містить усі літерали, імена методів та інші константи класу.

Два шляхи потрапляння рядка у пул:

  1. Літерали — автоматично при завантаженні класу: String s = "Hello";
  2. Метод 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 (один об'єкт у пулі)

Типові помилки

  1. Помилка: Використання new String("literal") без необхідності Рішення: Використовуйте літерали — String s = "literal";

  2. Помилка: Порівняння рядків через == замість 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

  1. Колізії в StringTable: Якщо кількість рядків » StringTableSize, пошук перетворюється з O(1) на O(n). Перевірте: jcmd <pid> VM.stringtable -verbose

  2. String Pool і GC: У Java 7+ рядки з пулу можуть бути видалені GC, якщо на них немає посилань. Але якщо ви утримуєте посилання — вони ніколи не зберуться.

  3. 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]]