Що таке дедуплікація рядків в G1 GC
У типових корпоративних застосунках мільйони рядків дублюються (статуси "ACTIVE", "SUCCESS", ролі користувачів, JSON-ключі, заголовки HTTP).
🟢 Junior Level
String Deduplication (дедуплікація рядків) — це фоновий механізм збирача сміття JVM, який автоматично знаходить у пам’яті купи об’єкти String з однаковим текстовим вмістом і перенаправляє їхні внутрішні масиви на один спільний масив байтів.
Суть у 30 секундах
У типових корпоративних застосунках мільйони рядків дублюються (статуси "ACTIVE", "SUCCESS", ролі користувачів, JSON-ключі, заголовки HTTP).
- Без дедуплікації кожен об’єкт
Stringзберігає свій власний дублікат масивуbyte[] value. - З увімкненою дедуплікацією JVM знаходить дублікати та звільняє зайві масиви байтів, залишаючи один екземпляр масиву для всіх однакових рядків.
- Це дає економію від 10% до 25% усієї пам’яті купи (Heap) без зміни жодного рядка вихідного Java-коду.
# Увімкнення дедуплікації рядків під час запуску JVM:
java -XX:+UseG1GC -XX:+UseStringDeduplication -jar app.jar
Ключова відмінність від String Pool (intern())
| Параметр | String Pool (intern()) | String Deduplication |
| :— | :— | :— |
| Що об’єднується | Самі об’єкти java.lang.String | Тільки внутрішні масиви byte[] value |
| Результат s1 == s2 | true (посилання на об’єкти стають рівними) | false (об’єкти залишаються різними!) |
| Як керується | Вручну викликом str.intern() у Java-коді | Автоматично фоновим потоком GC через прапорець JVM |
| Ризик OutOfMemoryError | ⚠️ Високий (у разі інтернування унікальних рядків) | ❌ Відсутній (GC сам керує пам’яттю) |
🟡 Middle Level
Життєвий цикл дедуплікації рядка
Дедуплікація не відбувається миттєво під час створення рядка (інакше це паралізувало б CPU). Процес розділений на етапи:
- Народження в Eden: Рядок створюється зі своїм власним масивом
byte[]. - Фільтрація за віком (
Age Threshold): Переважна більшість рядків помирає в поколінні Eden у перші мілісекунди. Дедуплікатор ігнорує молоді рядки, доки вони не переживуть кілька циклів збирання сміття (поріг за замовчуванням — 3 цикли GC, прапорець-XX:StringDeduplicationAgeThreshold=3). - Постановка в чергу: Рядки-кандидати, що вижили в Young GC, реєструються у спеціальній черзі дедуплікації (Deduplication Queue).
- Асинхронна обробка фоновим потоком (
StringDedupThread):- Обчислюється 32-бітний геш масиву
byte[] value. - Здійснюється пошук у нативній геш-таблиці дедуплікації (
Deduplication Tableу Native Memory). - Якщо геш збігся — виконується побайтове порівняння (для виключення колізій).
- Якщо масиви ідентичні — посилання
valueдругого об’єктаStringчерез атомарну операцію CAS перенаправляється на масив першого об’єкта. - Звільнений дублікат масиву забирається збирачем сміття в наступному циклі GC.
- Обчислюється 32-бітний геш масиву
До дедуплікації:
String A [Header] ──value──► byte[] [ 'H', 'e', 'l', 'l', 'o' ] (24 байти)
String B [Header] ──value──► byte[] [ 'H', 'e', 'l', 'l', 'o' ] (24 байти)
Після дедуплікації:
String A [Header] ──value──► byte[] [ 'H', 'e', 'l', 'l', 'o' ] (24 байти)
▲
String B [Header] ──value──────────┘
(Другий масив byte[] звільнений збирачем сміття!)
Підтримка у збирачах сміття: еволюція до Java 21
- Java 8 Update 20 – Java 17: Дедуплікація рядків підтримувалася тільки в G1 GC та Shenandoah GC. Збирачі Parallel GC, Serial GC та ZGC її не підтримували.
- Java 18+ (включно з Java 21 LTS): Механізм дедуплікації рядків був уніфікований і винесений із G1 у загальний шар GC (JDK-8267186). Тепер прапорець
-XX:+UseStringDeduplicationпідтримується в G1, Parallel, Serial та ZGC.
🔴 Senior Level
Архітектура HotSpot: Нативна таблиця та потокобезпека CAS
У вихідному коді OpenJDK HotSpot механізм дедуплікації реалізований мовою C++:
- Нативна таблиця дедуплікації (
StringDedupTable): Зберігається в пам’яті C-Heap (поза Java Heap) і займає від 15 до 50 МБ нативної пам’яті. Записи в таблиці утримуються слабкими вказівниками (OopHandle), тому таблиця не захищає об’єкти від збирання сміття. - Атомарна підміна через CAS:
Поле
valueвjava.lang.Stringпозначено як@Stable final. JVM здійснює підміну посилання на масив через атомарну інструкціюAtomic::cmpxchg(Compare-And-Swap) з бар’єром пам’яті StoreLoad/Release. Потік застосунку, який викликав у цей момент методcharAt()абоgetBytes(), гарантовано побачить або старий масив, або новий — оскільки їхній вміст байт-у-байт ідентичний, операція є 100% безпечною та непомітною для прикладного коду. - Обмеження за кодуванням (
codercheck): Дедуплікація об’єднує лише масиви з однаковою довжиною та однаковимcoder. Якщо рядок"Hello"маєcoder = LATIN1(5 байтів), а інший"Hello"випадково був створений якUTF16(10 байтів), вони дедупліковані не будуть.
Аналіз накладних витрат: CPU vs Memory Trade-off
Увімкнення -XX:+UseStringDeduplication не є «безкоштовним»:
- Накладні витрати CPU: Фоновий потік дедуплікації безперервно обчислює геші та виконує побайтові порівняння, споживаючи від 1.5% до 4% процесорного часу.
- Коли дедуплікація шкідлива: Якщо в застосунку переважають унікальні рядки (UUID, геші паролів, часові мітки), дедуплікатор витратить такти CPU на даремне гешування, не знайде збігів і заощадить 0% пам’яті.
- Коли дедуплікація ідеальна: REST-мікросервіси, агрегатори логів, ETL-пайплайни, де тисячі JSON-об’єктів містять повторювані назви полів, статуси та переліки.
4 Tricky Questions
1. Чи стане вираз s1 == s2 істинним (true) після того, як G1 GC успішно дедуплікує два однакові рядки?
Відповідь:
Ні, вираз s1 == s2 залишиться false.
String Deduplication працює на рівні внутрішніх масивів byte[] value. Збирач сміття робить спільним лише посилання на масив символів. Самі об’єкти String (їхні заголовки Mark Word, Klass Word, геш-коди та адреси в купі) залишаються ізольованими об’єктами.
Порівняння за посиланням s1 == s2 завжди порівнює адреси самих екземплярів String, тому повертає false. Порівняння за значенням s1.equals(s2) повертає true.
2. Чи підтримується String Deduplication у ZGC в сучасних версіях Java (Java 18–21)?
Відповідь:
Так, підтримується.
У старих версіях Java (Java 8–17) дедуплікація рядків була жорстко вбудована виключно в кодову базу G1 GC (та Shenandoah). Проте починаючи з Java 18, в OpenJDK було реалізовано проєкт уніфікації дедуплікації рядків (JDK-8267186). Механізм було перенесено до загального компонента віртуальної машини, і в сучасних версіях Java (включно з Java 21 LTS) прапорець -XX:+UseStringDeduplication повноцінно працює не лише в G1, але також у ZGC, Parallel GC та Serial GC.
3. Навіщо потрібен параметр -XX:StringDeduplicationAgeThreshold=3 і чому не можна дедуплікувати рядки відразу під час їх створення в Eden?
Відповідь: Згідно зі слабкою гіпотезою про покоління (Weak Generational Hypothesis), понад 95% рядків у Java-застосунках є ультракороткоживучими: вони створюються для формування логу, парсингу JSON або виконання методу і помирають за кілька мілісекунд у першому ж Minor GC. Якби JVM намагалася дедуплікувати рядки прямо в Eden:
- Процесор витрачав би до 30–40% усієї потужності на обчислення гешів і пошук у таблиці для об’єктів, які за частку секунди зникнуть.
- Поріг у 3 цикли збирання (Age Threshold) гарантує, що дедуплікуються лише довгоживучі рядки (Tenured / Old Gen), які реально осядуть у пам’яті й окуплять витрати процесорного часу на їх оптимізацію.
4. Як перевірити ефективність роботи дедуплікації рядків у продакшені без зупинки сервісу?
Відповідь:
- Діагностична команда
jcmd:jcmd <pid> GC.string_deduplication -dumpКоманда виводить детальну статистику: скільки рядків було проскановано, скільки дублікатів знайдено, скільки мегабайтів купи заощаджено і який відсоток успіху.
- Логування GC під час старту JVM:
Увімкнення тегу логування:
-Xlog:gc+stringdedup*=info:stdout:time,uptime,level,tags. У логах відображатиметься детальний звіт кожного запуску потоку дедуплікації:[GC concurrent-string-deduplication, 12.4ms, 150000 strings, 4.2MB (25.3%) saved].
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь
«String Deduplication — це прозора оптимізація збирача сміття (прапорець
-XX:+UseStringDeduplication), яка у фоновому режимі знаходить однакові рядки, що пережили кілька циклів GC, і перенаправляє їхнє полеvalueна один спільний масивbyte[]. На відміну відintern(), дедуплікація не об’єднує самі об’єктиString(==залишаєтьсяfalse), не вимагає зміни коду і не несе ризикуOutOfMemoryError. У типових корпоративних застосунках економить 10–25% пам’яті купи при оверхеді по CPU у 2–4%. Починаючи з Java 18, працює не лише в G1, але й у ZGC, Parallel та Serial GC».
Ключові тези для Senior-інтерв’ю
- Рівень оптимізації: Об’єднує масиви
byte[] value, але зберігає різні екземпляриString(==даєfalse). - Age Threshold: За замовчуванням 3 цикли GC; відсікає короткоживучі рядки з Eden для захисту CPU.
- CAS Update: Атомарна заміна вказівника на масив є безпечною для паралельно читаючих потоків.
- Уніфікація в Java 18+: Підтримується в G1, Shenandoah, ZGC, Parallel GC.
- CPU Overhead: Витрачає 2–4% CPU; не рекомендується для застосунків із переважно унікальними рядками (UUID, геші).
- Діагностика:
jcmd <pid> GC.string_deduplicationта-Xlog:gc+stringdedup*=debug.
Червоні прапорці (Чого говорити категорично не можна)
- ❌ Стверджувати: «Дедуплікація рядків робить оператор
s1 == s2істинним» (головна помилка: об’єкти залишаються різними). - ❌ Вважати дедуплікацію синонімом
String.intern()(різні механізми, пам’ять та цілі). - ❌ Вважати, що дедуплікуються всі рядки поспіль відразу в Eden (без урахування Age Threshold).
- ❌ Стверджувати, що String Deduplication працює «тільки і виключно в G1» (з Java 18 доступна і в ZGC/Parallel).