🔤 Section 12 · Question #22

What is String Deduplication in G1 GC

In typical enterprise applications, millions of duplicate strings reside in memory (e.g., status flags "ACTIVE", "SUCCESS", user roles, JSON keys, HTTP headers).


🟢 Junior Level

String Deduplication is an automated background garbage collector optimization that identifies String objects with identical character sequences in the Java heap and redirects their internal references to a single shared backing byte array.

In a Nutshell (30 Seconds)

In typical enterprise applications, millions of duplicate strings reside in memory (e.g., status flags "ACTIVE", "SUCCESS", user roles, JSON keys, HTTP headers).

  • Without deduplication, every String instance retains its own distinct copy of the byte[] value array.
  • With deduplication enabled, the JVM locates duplicates and frees redundant byte arrays, leaving a single shared array instance for all identical strings.
  • This typically yields 10% to 25% heap memory savings without modifying a single line of Java application code.
# Enabling String Deduplication on JVM launch:
java -XX:+UseG1GC -XX:+UseStringDeduplication -jar app.jar

Key Differences: String Deduplication vs. String Pool (intern())

Parameter String Pool (intern()) String Deduplication (-XX:+UseStringDeduplication)
What is Unified Entire java.lang.String object instances Only the internal backing byte[] value arrays
Result of s1 == s2 true (object references become identical) false (object references remain distinct!)
Management Manual invocation via str.intern() in application code Fully automated by GC background worker threads via JVM flag
OutOfMemoryError Risk ⚠️ High risk if interning unbounded dynamic values ❌ Zero risk (GC manages lifecycle and weak references)

🟡 Middle Level

Lifecycle of String Deduplication

Deduplication does not execute synchronously upon string creation (which would paralyze CPU throughput). The process is multi-staged:

  1. Allocation in Eden: The string is created with its own private byte[] value array.
  2. Filtering by Age (Age Threshold): The vast majority of strings die in the Eden space within milliseconds (Weak Generational Hypothesis). The deduplicator ignores young strings until they survive multiple garbage collection cycles (default threshold: 3 GC cycles, controlled via -XX:StringDeduplicationAgeThreshold=3).
  3. Queue Registration: Candidate strings surviving Young GC evacuation passes are registered into a native Deduplication Queue.
  4. Asynchronous Processing by StringDedupThread:
    • Computes a 32-bit hash of the byte[] value array.
    • Performs a lookup in the native off-heap hash table (Deduplication Table in C-Heap).
    • If a matching hash is found, executes a byte-by-byte comparison to verify character identity and rule out hash collisions.
    • If identical, performs an atomic CAS (Compare-And-Swap) operation to redirect the second string’s value pointer to the first string’s array.
    • The unreferenced duplicate array is reclaimed by the garbage collector in the subsequent GC cycle.
Before Deduplication:
String A [Header] ──value──► byte[] [ 'H', 'e', 'l', 'l', 'o' ] (24 bytes)
String B [Header] ──value──► byte[] [ 'H', 'e', 'l', 'l', 'o' ] (24 bytes)

After Deduplication:
String A [Header] ──value──► byte[] [ 'H', 'e', 'l', 'l', 'o' ] (24 bytes)
                                   ▲
String B [Header] ──value──────────┘
(The second byte[] array is reclaimed during the next GC cycle!)

Garbage Collector Support: Evolution to Java 21

  • Java 8 Update 20 through Java 17: String Deduplication was historically implemented exclusively within G1 GC (and Shenandoah GC). Parallel GC, Serial GC, and ZGC did not support it.
  • Java 18 and Later (including Java 21 LTS): Under JDK-8267186, OpenJDK unified String Deduplication across the entire JVM runtime. The -XX:+UseStringDeduplication flag is now fully supported across G1, ZGC, Parallel GC, and Serial GC.

🔴 Senior Level

HotSpot Architecture: Native Table and CAS Concurrency

In OpenJDK HotSpot, String Deduplication is implemented in C++:

  1. Native Off-Heap Deduplication Table (StringDedupTable): Resides in C-Heap memory outside the Java Heap, consuming roughly 15 to 50 MB of native memory. Entries are referenced via weak handles (OopHandle), ensuring that table entries never prevent objects from being garbage-collected.
  2. Atomic CAS Pointer Swap: Although String.value is declared @Stable final, the JVM swaps array references using native Atomic::cmpxchg (Compare-And-Swap) instructions with StoreLoad/Release memory fences. Any application thread concurrently invoking charAt() or getBytes() observes either the previous array or the new array. Because the arrays contain identical bytes, the operation is 100% thread-safe and completely transparent to application logic.
  3. Encoding Constraints (coder check): Deduplication merges arrays only if they share identical lengths and the same coder flag. If one "Hello" string is Latin-1 (5 bytes) and another "Hello" was constructed as UTF-16 (10 bytes), they cannot be deduplicated.

Performance Analysis: CPU vs. Memory Trade-Off

Enabling -XX:+UseStringDeduplication carries an operational cost:

  • CPU Overhead: The background deduplication worker thread consumes between 1.5% and 4% of total CPU capacity for hashing and byte comparisons.
  • When Deduplication Degrades Performance: In systems dominated by unique strings (UUIDs, cryptographic tokens, high-cardinality timestamps), the deduplicator burns CPU cycles computing hashes without finding duplicates, resulting in 0% memory savings.
  • When Deduplication Shines: REST API gateways, log aggregators, message brokers, and ETL pipelines where thousands of JSON payloads contain repetitive dictionary keys, status enums, and country codes.

4 Tricky Questions

1. Does the expression s1 == s2 evaluate to true after G1 GC successfully deduplicates two identical strings?

Answer: No, s1 == s2 remains false. String Deduplication operates exclusively on the internal byte[] value array. The garbage collector shares only the backing byte buffer. The java.lang.String object wrappers (including their object headers, identity hashcodes, and distinct heap memory addresses) remain completely separate instances. The reference equality check s1 == s2 compares the memory addresses of the String wrapper objects, evaluating to false. Value equality s1.equals(s2) evaluates to true.

2. Is String Deduplication supported in ZGC in modern Java (Java 18–21)?

Answer: Yes, it is fully supported. While String Deduplication was originally restricted to G1 GC (and Shenandoah) in Java 8u20 through Java 17, OpenJDK refactored the mechanism in Java 18 (JDK-8267186) into a GC-agnostic platform feature. In Java 18, 21 LTS, and later releases, passing -XX:+UseStringDeduplication works seamlessly with ZGC, G1 GC, Parallel GC, and Serial GC.

3. Why does -XX:StringDeduplicationAgeThreshold=3 exist, and why not deduplicate strings immediately in Eden?

Answer: Under the Weak Generational Hypothesis, over 95% of strings in Java enterprise applications are transient: they are allocated for JSON parsing, logging, or method parameters, and collected within milliseconds during Minor GC. If the JVM attempted to deduplicate strings immediately in Eden:

  • The CPU would waste up to 30–40% of its processing power hashing and checking objects that will disappear moments later.
  • Setting an Age Threshold of 3 GC cycles ensures that only tenured, long-lived strings surviving multiple collection passes are considered for deduplication, maximizing memory reclamation while keeping CPU overhead below 3%.

4. How can you verify String Deduplication efficiency in a live production environment without restarting the JVM?

Answer:

  1. Via jcmd Diagnostic Command:
    jcmd <pid> GC.string_deduplication -dump
    

    This outputs comprehensive operational statistics: total strings inspected, number of duplicates detected, megabytes of heap reclaimed, and the deduplication success percentage.

  2. Via Unified GC Logging: Enable logging at JVM startup: -Xlog:gc+stringdedup*=info:stdout:time,uptime,level,tags Every deduplication run will output an event: [GC concurrent-string-deduplication, 12.4ms, 150000 strings, 4.2MB (25.3%) saved].

🎯 Interview Cheat Sheet

30-Second Summary

“String Deduplication (-XX:+UseStringDeduplication) is a GC optimization that runs in a background thread, identifying strings with identical text that have survived multiple GC passes and merging their internal byte[] value arrays. Unlike String.intern(), it does not merge the String objects themselves (== remains false), requires zero application code changes, and eliminates OutOfMemoryError risks. In enterprise services, it typically saves 10–25% of heap memory for a 2–4% CPU trade-off. Since Java 18, it is supported across G1, ZGC, Parallel, and Serial GC.”

Key Takeaways for Senior Interviews

  • Array-Level Optimization: Merges backing byte[] value arrays; String wrappers remain distinct (== is false).
  • Age Threshold: Defaults to 3 GC cycles (-XX:StringDeduplicationAgeThreshold=3), protecting Eden from wasteful hashing.
  • Atomic CAS Update: Thread-safe pointer replacement invisible to concurrent reading threads.
  • Java 18+ Unification: Supported across G1, ZGC, Parallel, and Serial collectors (JDK-8267186).
  • CPU Trade-off: Consumes 2–4% CPU; avoid in services dominated by unique strings (UUIDs, cryptographic tokens).
  • Production Observability: Inspect using jcmd <pid> GC.string_deduplication and -Xlog:gc+stringdedup*=info.

Red Flags to Avoid

  • ❌ Claiming that String Deduplication makes s1 == s2 return true (classic mistake; objects remain distinct).
  • ❌ Confusing String Deduplication with String.intern() (interning unifies objects and mutates StringTable).
  • ❌ Assuming deduplication runs immediately on every new string in Eden.
  • ❌ Claiming that String Deduplication only works in G1 GC (it works with ZGC and Parallel GC in Java 18+).