🔤 Section 12 · Question #19

What are Compact Strings in Java 9+

Compact Strings is enabled by default and operates 100% transparently to application code.


🟢 Junior Level

Compact Strings (JEP 254) is a foundational memory optimization for internal string storage introduced in Java 9 that cuts the memory footprint of strings containing Latin/ASCII characters in half.

In a Nutshell (30 Seconds)

  • Before Java 9 (Java 1.0 – 8): Every string inside the JVM was stored as an array of 16-bit characters (char[] value) encoded in UTF-16. Every single character occupied 2 bytes, even for standard Latin letters 'a', 'b', 'c', digits, or JSON delimiters, whose upper 8 bits were always zero (0x00).
  • Java 9 and Later (Java 11, 17, 21+): The char[] array was replaced by a byte array byte[] value accompanied by a single-byte encoding flag coder:
    • If all characters fit within the Latin-1 range (U+0000 .. U+00FF) $\to$ the string uses 1 byte per character (coder = 0).
    • If the string contains even a single non-Latin character (Cyrillic, Chinese, emojis, mathematical symbols) $\to$ the string switches to UTF-16 using 2 bytes per character (coder = 1).
// Memory allocation comparison in Java 9+:
String latin = "Hello World"; // 11 characters -> 11 bytes in the backing value array (coder = 0)
String rus   = "Привет Мир";  // 10 characters -> 20 bytes in the backing value array (coder = 1)

Compact Strings is enabled by default and operates 100% transparently to application code.


🟡 Middle Level

Internal Class Layout: java.lang.String

In Java 9+, the fields of java.lang.String are declared as follows:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    @Stable
    private final byte[] value; // Backing byte storage
    private final byte coder;   // Encoding selector: 0 (LATIN1) or 1 (UTF16)
    private int hash;           // Cached 32-bit hashcode
    
    static final byte LATIN1 = 0;
    static final byte UTF16  = 1;
}

High-Speed Bit-Shift Arithmetic

Because character widths are powers of two (1 byte vs. 2 bytes), HotSpot executes length calculations and indexing using fast single-cycle bitwise shifts (>> and <<):

// Calculating string length:
public int length() {
    // If coder == 0 (LATIN1): value.length >> 0 = value.length
    // If coder == 1 (UTF16):  value.length >> 1 = value.length / 2
    return value.length >> coder; 
}

// Accessing character at index:
public char charAt(int index) {
    if (isLatin1()) {
        return (char)(value[index] & 0xff); // Zero-extends 1 byte to 16-bit char
    }
    return StringUTF16.getChar(value, index); // Combines 2 consecutive bytes
}

String Inflation (Widening)

The encoding format is homogeneous for the entire string. A single String instance cannot store mixed representations where some characters are Latin-1 and others are UTF-16. Appending a single non-Latin character causes Inflation:

String prefix = "User_";      // coder = LATIN1 (5 bytes)
String name   = "Иван";       // coder = UTF16 (8 bytes)
String full   = prefix + name; // RESULT: coder = UTF16!
// Length = 9 characters -> backing array size = 18 bytes!
// The ASCII prefix "User_" is widened to consume 2 bytes per character.

Memory Footprint Comparison

String Content Character Count Java 8 (char[]) Java 9+ (byte[]) Backing Array Memory Savings
"Java" (ASCII) 4 8 bytes 4 bytes 50%
"HTTP/1.1 200 OK" 15 30 bytes 15 bytes 50%
"order_id_102" 12 24 bytes 12 bytes 50%
"Интервью" (Cyrillic) 8 16 bytes 16 bytes 0% (Requires UTF-16)
"User: Иван" (Mixed) 10 20 bytes 20 bytes 0% (Inflated to UTF-16)

🔴 Senior Level

Historical Context: Why Java 6 -XX:+UseCompressedStrings Failed

An early attempt at compressing strings was introduced in Java 6 Update 21 via the experimental flag -XX:+UseCompressedStrings. It was completely removed in Java 7.

Why did the Java 6 implementation fail?

  1. In Java 6, compression was a localized hack applied strictly to String. Classes like StringBuilder, StringBuffer, and internal I/O decoders remained coupled to char[].
  2. Every string modification or concatenation required continuous repacking from byte[] to char[] and back, degrading CPU throughput by up to 30–40%.
  3. Why did Java 9 (JEP 254) succeed? Architects redesigned the entire JDK hierarchy: AbstractStringBuilder, StringBuilder, StringBuffer, and character conversion intrinsics were migrated to natively support byte[] value paired with coder. No redundant packing/unpacking cycles take place.

Hardware SIMD Vector Intrinsics in HotSpot C2

HotSpot C2 substitutes methods in StringLatin1 and StringUTF16 with native assembly SIMD instructions (AVX-512 / AVX2 on x86-64, NEON on ARM64):

  • StringUTF16.compress: Scans sixteen 16-bit characters per CPU cycle, verifies that all upper bytes are 0x00, and packs them into sixteen 8-bit bytes in a single instruction.
  • StringLatin1.inflate: Unpacks an 8-bit byte array into 16-bit words by interleaving zero bytes at memory bus line speed.
  • StringLatin1.equals: Compares strings in parallel blocks of 32 or 64 bytes per clock cycle.

The @Stable Annotation and Constant Folding

Notice the field declaration in String.java:

@Stable
private final byte[] value;

In Java, marking an array final guarantees only that the array reference cannot be reassigned; array elements remain mutable. The JVM-internal @Stable annotation gives the C2 JIT compiler a powerful optimization guarantee: array elements will never change once initialized. When a string reference is recognized as constant (Constant Folding), C2 inlines array bytes directly into the compiled machine code as native immediate values, eliminating memory reads entirely.

Object Layout & Heap Alignment

On a 64-bit JVM with Compressed OOPs enabled (-XX:+UseCompressedOops):

  • Mark Word: 8 bytes
  • Klass Word: 4 bytes
  • Array reference value: 4 bytes
  • coder field: 1 byte
  • hash field: 4 bytes
  • hashIsZero field (Java 13+): 1 byte
  • 8-byte boundary padding: 2 bytes Total String instance overhead: exactly 24 bytes (identical to Java 7/8). The immense macro-level heap reduction (20% to 30% of total JVM heap across enterprise services) comes entirely from shrinking the backing byte[] array by half.

4 Tricky Questions

1. How do Compact Strings in Java 9 (JEP 254) fundamentally differ from the experimental -XX:+UseCompressedStrings flag in Java 6?

Answer:

  • In Java 6, compression was isolated to the String class alone. Builders (StringBuilder, StringBuffer) and I/O buffers still operated on char[]. Every concatenation or string mutation forced cyclic unpacking to char[] and packing back to byte[], destroying CPU throughput.
  • In Java 9, the entire platform (AbstractStringBuilder, StringBuilder, StringBuffer, standard charsets, and HotSpot JIT intrinsics) was redesigned to operate natively on byte[] and coder. Transcoding is hardware-accelerated via CPU SIMD vector instructions, eliminating CPU regression.

2. How many bytes in the heap does the string "Hello 123" occupy in Java 8 vs. Java 17 (on a 64-bit JVM with Compressed OOPs)?

Answer: The string has a length of 9 characters (pure ASCII / Latin-1).

  • In Java 8:
    • String object: 24 bytes.
    • Backing array char[9]: Array header (16 bytes) + $9 \times 2$ bytes = 34 bytes $\to$ padded to 8-byte multiple = 40 bytes.
    • Total in Java 8: 64 bytes.
  • In Java 17:
    • String object: 24 bytes.
    • Backing array byte[9] (coder = LATIN1): Array header (16 bytes) + $9 \times 1$ byte = 25 bytes $\to$ padded to 8-byte multiple = 32 bytes.
    • Total in Java 17: 56 bytes. This yields a net saving of 8 bytes for this short 9-character string (~12.5%). For strings longer than 50 characters, memory savings approach 50%.

3. Does substring() demote the encoding from UTF-16 back to Latin-1 when slicing an ASCII substring out of a Cyrillic or mixed string?

Answer: No, it does not demote. In OpenJDK HotSpot, substring() inherits the coder flag of the source string:

String source = "Order: 12345 (Заказ)"; // coder = UTF16
String sub = source.substring(0, 12);    // "Order: 12345"

Even though the extracted substring "Order: 12345" consists purely of ASCII characters, its internal coder remains UTF16, consuming 2 bytes per character. HotSpot avoids scanning the extracted array upon every substring() invocation because an $O(N)$ validation pass would degrade performance.

4. Can you explicitly disable Compact Strings in modern Java, and is there any legitimate reason to do so?

Answer: Yes, it can be disabled using the command-line flag: -XX:-CompactStrings When disabled, the JVM forces coder = UTF16 for every string, reverting to Java 8 behavior. Is there a legitimate reason? The only theoretical scenario is a system operating in regions with non-Latin scripts (e.g., pure Chinese, Japanese, or Arabic text processing) where 100% of strings require UTF-16. In that specialized case, disabling the flag avoids the minor branch check if (coder == LATIN1) in string operations. For general web services, disabling Compact Strings is strongly discouraged.


🎯 Interview Cheat Sheet

30-Second Summary

“Compact Strings (JEP 254 in Java 9+) replaces char[] with a byte[] value array and a coder byte flag (0 for Latin-1, 1 for UTF-16). When a string contains only Latin-1 characters ($\le \text{U+00FF}$), each character consumes 1 byte instead of 2, saving 50% on array memory. If non-Latin characters (Cyrillic, Asian scripts, emojis) are introduced, the string inflates to UTF-16 (2 bytes per character). In enterprise applications handling JSON, HTTP headers, and logs, this saves 20–30% of total heap memory with zero code modifications.”

Key Takeaways for Senior Interviews

  • Internal Representation: byte[] value + byte coder (0 = Latin-1, 1 = UTF-16).
  • Fast Bitwise Shifts: Length is computed via value.length >> coder without conditional branches.
  • String Inflation: Adding a single non-Latin character widens the entire string to UTF-16.
  • SIMD Vectorization: Operations like compression, inflation, and comparison use hardware vector intrinsics (AVX-512 / NEON).
  • JVM Control: Enabled by default; can be disabled with -XX:-CompactStrings.
  • The @Stable Annotation: Enables C2 Constant Inlining for immutable array elements.

Red Flags to Avoid

  • ❌ Calling Compact Strings “compression like GZIP or ZIP” (it is an 8-bit vs 16-bit storage representation, not a compression algorithm).
  • ❌ Claiming that Cyrillic or Asian characters are compressed by Compact Strings (they require UTF-16).
  • ❌ Believing that -XX:+CompactStrings is required to turn it on (it is on by default since Java 9).
  • ❌ Assuming that substring() automatically demotes UTF-16 strings back to Latin-1.