Что такое String deduplication в G1 GC?
4. Объединение: Если найден идентичный массив, поле value перенаправляется на существующий массив через атомарную операцию
🟢 Junior Level
String Deduplication (дедупликация строк) — это функция сборщика мусора G1, которая автоматически находит одинаковые строки в памяти и объединяет их внутренние массивы, чтобы сэкономить память.
Как это работает:
- JVM замечает, что два разных объекта
Stringсодержат одинаковый текст - Вместо того чтобы хранить два одинаковых массива байт, она заставляет обе строки ссылаться на один массив
- Это происходит автоматически — вам не нужно менять код
Как включить:
java -XX:+UseG1GC -XX:+UseStringDeduplication -jar app.jar
Простая аналогия: Представьте, что у вас 100 копий одной и той же книги в библиотеке. Дедупликация — это когда библиотекарь оставляет одну книгу на полке, а остальным читателям даёт ссылку на неё. Текст тот же, но место экономится.
Отличие от String Pool:
| | String Pool (intern()) | String Deduplication |
| —————– | ————————– | ————————— |
| Что объединяет | Объекты String | Внутренние byte[] массивы |
| Нужно менять код? | Да (str.intern()) | Нет (только JVM-флаг) |
| Когда работает | При вызове intern() | Во время GC (фоном) |
🟡 Middle Level
Как это работает внутри
- Сканирование: Во время GC (evacuation phase) G1 помечает объекты
Stringв собираемых регионах - Очередь: Ссылки на строки-кандидаты помещаются в очередь дедупликации
- Фоновый поток: Отдельный поток вычисляет хеш
byte[]и ищет совпадения в deduplication table - Объединение: Если найден идентичный массив, поле
valueперенаправляется на существующий массив через атомарную операцию
Отличие от String Pool — детальное сравнение
| Характеристика | String Pool (intern()) |
String Deduplication |
|---|---|---|
| Что объединяет | Объекты String |
Внутренние byte[] массивы |
| Когда | При вызове intern() (синхронно) |
Во время GC (асинхронно) |
| Управление | Ручное (нужно вызывать intern()) |
Автоматическое (JVM-флаг) |
| Работает с любыми GC | Да | Только G1 GC и Shenandoah |
Влияние на == |
Делает == true |
== остаётся false (объекты разные) |
| CPU overhead | При каждом intern() |
Фоновый поток, ~2–5% CPU |
| Память table | StringTable в Heap (~32 bytes/entry) | Native memory (~10–50MB) |
Когда включать
- Профилировщик показывает много дублирующихся строк в Heap
- Вы не можете использовать
intern()(legacy-код, сложная логика, нет доступа к коду) - Приложение работает на G1 GC (по умолчанию в Java 9+)
- Heap > 4GB и строки занимают значительную часть
Таблица типичных ошибок
| Ошибка | Последствия | Решение |
|---|---|---|
| Ожидание мгновенного результата | «Включил, а память не освободилась» | Дедупликация происходит во время GC, не мгновенно; нужно несколько GC-циклов |
| Включение без мониторинга | Неизвестно, работает ли вообще | Проверьте через -XX:+PrintStringDeduplicationStatistics |
Ожидание, что == станет true |
Логика сравнения сломана | Дедупликация не меняет ссылки на объекты, только внутренние массивы |
| Включение на ZGC | Не работает | ZGC пока не поддерживает String Deduplication |
Когда НЕ использовать
- Мало дубликатов: если строки в основном уникальные — overhead без профита
- Short-lived строки: умирают до попадания в очередь дедупликации
- ZGC: не поддерживается (используйте
-XX:+UseStringDeduplicationс Shenandoah) - Ультра-low-latency: CPU overhead 2–5% может быть критичен
🔴 Senior Level
Internal Implementation — G1 GC Deduplication Pipeline
┌──────────────────────────────────────────────────────────────┐
│ GC (Evacuation Phase) │
│ ├── Identify String objects in collection set │
│ ├── Filter: age >= DeduplicationAgeThreshold (default 3) │
│ ├── Enqueue candidates to dedup queue │
│ └── Continue evacuation │
├──────────────────────────────────────────────────────────────┤
│ Deduplication Thread (concurrent, low-priority) │
│ ├── Dequeue String references │
│ ├── Compute hash of byte[] value (age hash, not String.hashCode) │
│ ├── Lookup in deduplication table (native memory hashtable) │
│ ├── If found: побайтовое сравнение для подтверждения │
│ ├── If match: CAS redirect value reference → shared array │
│ └── If not found: add to table │
└──────────────────────────────────────────────────────────────┘
Deduplication Table:
- Нативная хеш-таблица (вне Java Heap, в C-heap)
- Хранит хеши
byte[]массивов + weak references - При совпадении хешей — побайтовое сравнение для подтверждения (защита от коллизий)
- Обновление ссылки через CAS (Compare-And-Swap) — thread-safe без блокировок
Marking Algorithm:
- G1 использует concurrent marking
- Строки помечаются во время marking phase
- Age threshold (по умолчанию 3 GC-цикла) — дедуплицируются только достаточно «пережившие» строки
- Это отсеивает short-lived строки, которые умрут до обработки очереди
Trade-offs
Плюсы:
- Прозрачность: не требует изменения кода — только JVM-флаг
- Экономия: 10–20% Heap для приложений с большим количеством текста
- Безопасность: нет риска corruption String Pool (данные не меняются)
- Работает с любыми строками, не только интернированными
Минусы:
- CPU overhead: хеширование + lookup + побайтовое сравнение (~2–5% CPU)
- Память: deduplication table (~10–50MB native memory)
- Только G1 GC (и Shenandoah в OpenJDK)
- Задержка: дедупликация происходит через несколько GC циклов (age threshold)
- Не дедуплицирует: строки с разными
coder(Latin-1 vs UTF-16 — массивы разной длины)
Edge Cases (минимум 3)
1. Не дедуплицирует строки с разными coder:
String s1 = "Hello"; // Latin-1, byte[5]
String s2 = new String("Hello".getBytes(StandardCharsets.UTF_16), StandardCharsets.UTF_16); // UTF-16, byte[12]
// Разные coder → разные byte[] массивы → дедупликация НЕ сработает
// Даже если содержание одинаковое, массивы имеют разный размер и байты
2. Очень короткоживущие строки:
void process() {
String temp = "duplicate"; // Eden
String temp2 = "duplicate"; // Eden
// Обе строки умирают в Young GC — не доживают до age threshold (3 GC)
// Дедупликация не успеет сработать
}
3. Race condition при redirect:
// Dedup thread выполняет CAS:
// if (CAS(oldValue, sharedValue)) → success
// Если два потока одновременно пытаются redirect — только один succeeds
// Другой поток видит, что value уже redirect-нут, и пропускает
// Поток, читающий s.value, всегда видит консистентное значение (@Stable + CAS)
// @Stable — JVM-аннотация, сообщающая JIT, что поле записывается один раз при конструировании.
4. String Pool vs Deduplication — взаимодействие:
String s1 = new String("Hello");
String s2 = new String("Hello");
// s1.value и s2.value — разные byte[] массивы (оба Latin-1, byte[5])
// После дедупликации: s1.value и s2.value → ОДИН и тот же byte[]
// Но s1 != s2 (объекты String разные!)
// Экономия: один byte[5] вместо двух → 5 bytes
5. Субнормальные строки (очень длинные):
String huge = "A".repeat(10_000_000); // 10MB
String huge2 = "A".repeat(10_000_000); // 10MB
// Побайтовое сравнение 10MB — expensive (~10ms)
// Deduplication thread может замедлить GC
// На практике: длинные строки могут быть идентичными в log aggregators или data pipelines
// (одинаковые JSON payloads), но это редкость для typical web applications.
Производительность
| Метрика | Без дедупликации | С дедупликацией | Delta |
|---|---|---|---|
| Heap usage | 4.0 GB | 3.2 GB | -20% |
| GC pause (avg) | 50ms | 55ms | +10% |
| CPU overhead | Baseline | +2–5% | Small |
| Young GC | 10ms | 10ms | No change |
| Mixed GC | 50ms | 55ms | +5ms |
| Native memory (table) | 0MB | 10–50MB | Extra |
Экономия памяти (реальные сценарии):
- JSON API сервис: 15–25% строк — дубликаты (keys, status values)
- Log aggregator: 30–40% дубликатов (levels, service names, host names)
- ETL pipeline: 5–10% дубликатов (category names, country codes)
Thread Safety
Дедупликация thread-safe:
- Обновление ссылки
valueчерез CAS (атомарная операция) valueполе —@Stable, но JVM позволяет redirect в рамках GC- Читающие потоки всегда видят консистентное значение (memory barriers при GC)
- Deduplication thread — один (single-threaded), нет contention между dedup-потоками
Production War Story
Сценарий 1: JSON API сервис (G1 GC, 8GB Heap, Spring Boot, 50K RPS):
- Без дедупликации: Heap usage 6.5GB, Full GC каждые 30 минут, p99 latency = 25ms
- С дедупликацией: Heap usage 5.2GB, Full GC каждые 50 минут, p99 latency = 20ms
- CPU overhead: +3% (приемлемо)
- Stats: deduplicated 2.3GB строк, 850K unique byte[] массивов объединены
- Результат: сокращение инстансов с 10 до 8 (экономия $15K/мес)
Сценарий 2: Log aggregator (1M log lines/min, G1 GC, 12GB Heap):
- Поля
level,service,host— много дубликатов ("INFO","UserService","host-1") - Без дедупликации: 12GB Heap
- С дедупликацией: 9GB Heap
- Экономия: 3GB → меньше инстансов в кластере
- Проблема: CPU overhead вырос до 7% из-за огромного количества строк. Fix:
-XX:StringDeduplicationAgeThreshold=5(увеличили age threshold, меньше кандидатов → меньше CPU).
Сценарий 3 (anti-pattern): Команда включила дедупликацию «на всякий случай» для приложения с уникальными строками (UUID, hash, timestamps). CPU overhead +4%, экономия памяти 0.5%. Отключили.
Monitoring
# Включить статистику дедупликации
-XX:+PrintStringDeduplicationStatistics
-XX:+PrintGC
# Вывод в GC логах:
# [GC concurrent string deduplication]
# String Deduplication: 1.2GB deduplicated (500K strings)
# [DEDUP: 500K strings, 1.2GB, 2.3ms]
# JCmd — статистика в runtime
jcmd <pid> GC.string_deduplication_statistics
# Вывод:
# String Deduplication Statistics:
# Executed: 1234 times
# Deduplicated: 567890 strings (1.2GB)
# Skipped: 123456 strings (already deduplicated)
# JFR (Java Flight Recorder)
java -XX:StartFlightRecording=filename=recording.jfr ...
# Events: StringDeduplicationStatistics
# В JDK Mission Controller: Memory → String Deduplication
# Настройка age threshold
-XX:StringDeduplicationAgeThreshold=3 # По умолчанию 3 GC-цикла
# Увеличьте до 5–10, если CPU overhead слишком высокий
Best Practices для Highload
- Включайте когда профилировщик показывает > 10% строк-дубликатов в Heap
- Не используйте как замену
intern()для высокодублирующихся long-lived строк (словари, конфиги) —intern()эффективнее - Мониторьте CPU overhead — если > 5%, увеличьте
-XX:StringDeduplicationAgeThreshold=5 - Комбинируйте:
intern()для dictionary data (enum values, status codes) + deduplication для всего остального - Для ZGC: не поддерживается (ZGC (as of JDK 21): не поддерживает String Deduplication) — рассмотрите Shenandoah (
-XX:+UseShenandoahGC -XX:+UseStringDeduplication) - Для max экономии: настройте
-XX:G1HeapRegionSize— меньшие регионы → чаще evacuation → больше кандидатов на дедупликацию - Не включайте для short-lived приложений (CLI, batch jobs < 1min) — не успеет сработать
- Для ultra-low-latency: benchmark с и без дедупликации; иногда 5ms GC pause increase критичен
🎯 Шпаргалка для интервью
Обязательно знать:
- String Deduplication — функция G1 GC, автоматически объединяет одинаковые
byte[]массивы строк - Включается флагом:
-XX:+UseG1GC -XX:+UseStringDeduplication(не требует изменения кода) - Отличие от String Pool: дедупликация объединяет
byte[], а не объекты String;==остаётся false - Работает асинхронно во время GC, age threshold (по умолчанию 3 GC-цикла) — отсеивает short-lived строки
- CPU overhead: ~2-5%, native memory для dedup table: ~10-50MB
- Не дедуплицирует строки с разными coder (Latin-1 vs UTF-16)
Частые уточняющие вопросы:
- Чем дедупликация отличается от
intern()? —intern()объединяет объекты String (== становится true), требует кода. Дедупликация — толькоbyte[]массивы,==остаётся false, без изменения кода. - Какая экономия памяти? — 10-20% Heap для приложений с дублирующимися строками. JSON API: 15-25%, log aggregator: 30-40%.
- Почему не работает с ZGC? — ZGC (as of JDK 21) не поддерживает String Deduplication. Альтернатива: Shenandoah GC.
- Как уменьшить CPU overhead? — Увеличить
-XX:StringDeduplicationAgeThreshold=5— меньше кандидатов, меньше CPU.
Красные флаги (НЕ говорить):
- ❌ “Дедупликация делает
==true” — объекты String остаются разными, объединяются толькоbyte[] - ❌ “Это замена
intern()” — нет,intern()эффективнее для dictionary data - ❌ “Работает мгновенно” — происходит во время GC, нужно несколько циклов
- ❌ “Работает с любым GC” — только G1 GC и Shenandoah
Связанные темы:
- [[1. Как работает String Pool]]
- [[3. Когда стоит использовать intern()]]
- [[19. Что такое compact strings в Java 9+]]
- [[20. Как узнать, сколько памяти занимает String]]