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 arraybyte[] valueaccompanied by a single-byte encoding flagcoder:- 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).
- If all characters fit within the Latin-1 range (
// 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?
- In Java 6, compression was a localized hack applied strictly to
String. Classes likeStringBuilder,StringBuffer, and internal I/O decoders remained coupled tochar[]. - Every string modification or concatenation required continuous repacking from
byte[]tochar[]and back, degrading CPU throughput by up to 30–40%. - Why did Java 9 (JEP 254) succeed?
Architects redesigned the entire JDK hierarchy:
AbstractStringBuilder,StringBuilder,StringBuffer, and character conversion intrinsics were migrated to natively supportbyte[] valuepaired withcoder. 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 are0x00, 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 coderfield: 1 bytehashfield: 4 byteshashIsZerofield (Java 13+): 1 byte- 8-byte boundary padding: 2 bytes
Total
Stringinstance 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 backingbyte[]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
Stringclass alone. Builders (StringBuilder,StringBuffer) and I/O buffers still operated onchar[]. Every concatenation or string mutation forced cyclic unpacking tochar[]and packing back tobyte[], destroying CPU throughput. - In Java 9, the entire platform (
AbstractStringBuilder,StringBuilder,StringBuffer, standard charsets, and HotSpot JIT intrinsics) was redesigned to operate natively onbyte[]andcoder. 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:
Stringobject: 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:
Stringobject: 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 abyte[] valuearray and acoderbyte 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 >> coderwithout 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
@StableAnnotation: 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:+CompactStringsis 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.
Related Questions
- What is String Encoding — Encodings, code points, and code units
- How to Find Out How Much Memory a String Occupies — Exact heap calculations with headers
- What substring() Does and How It Worked Before Java 7 — Substring memory behavior
- What is String Deduplication in G1 GC — Complementary heap optimization