Что такое String deduplication в 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. Теперь флаг
-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).
- ❌ Утверждать в 2024 году, что String Deduplication работает «только и исключительно в G1» (с Java 18 доступна и в ZGC/Parallel).