How Does String Pool Work and How is It Related to Immutability
The String Constant Pool is a specialized storage area in the Java Heap memory that implements the Flyweight Pattern to eliminate redundant string objects in memory.
🟢 Junior Level
30-Second Summary
The String Constant Pool is a specialized storage area in the Java Heap memory that implements the Flyweight Pattern to eliminate redundant string objects in memory.
When a string literal is declared (e.g., String s = "Java";):
- The JVM inspects the pool to check if an identical character sequence already exists.
- If present, the JVM returns a reference to the existing instance in the heap without allocating new memory.
- If absent, a new
Stringobject is allocated, registered in the pool, and returned.
Why the String Pool is Physically Impossible Without Immutability
Suppose String were mutable (like StringBuilder):
String s1 = "admin";
String s2 = "admin"; // s1 and s2 reference the EXACT SAME object in the String Pool
// If strings were mutable:
s1.setCharAt(0, 'u'); // Mutating "admin" to "udmin" via s1
Because s1 and s2 share the exact same heap reference, s2 (and every security module, SQL query builder, or framework thread in the JVM referencing "admin") would silently observe "udmin". This would lead to catastrophic security exploits and systemic crashes.
Core Invariant: Sharing a single object instance across millions of independent threads is safe only because strings are strictly immutable.
Code Example: Literal vs. Heap Object
public class StringPoolDemo {
public static void main(String[] args) {
String s1 = "Hello";
String s2 = "Hello"; // Reused from String Pool
String s3 = new String("Hello"); // Forces distinct allocation in regular Heap
System.out.println(s1 == s2); // true (Identical memory address)
System.out.println(s1 == s3); // false (Different heap references)
System.out.println(s1.equals(s3)); // true (Equal character content)
}
}
🟡 Middle Level
Bytecode Mechanics: Constant Pool and the ldc Opcode
During compilation, javac places string literals into the .class file’s Constant Pool (CONSTANT_Utf8_info).
At runtime, the JVM loads these references using the ldc (Load Constant) bytecode instruction:
0: ldc #2 // Loads reference to "Hello" from String Pool
2: astore_1 // Stores reference into local variable s1
3: new #3 // Allocates new java/lang/String object on Heap
6: dup
7: ldc #2 // Loads "Hello" literal to pass to constructor
9: invokespecial #4 // Invocations: String(String) constructor
12: astore_3
Dynamic Pool Population: String.intern()
The intern() method moves dynamically generated strings (from databases, files, or network payloads) into the String Pool:
String dynamicString = new String("custom"); // Created on heap outside pool
String pooledString = dynamicString.intern(); // Returns canonical pooled reference
System.out.println(pooledString == "custom"); // true!
If the string already exists in the pool, intern() returns the pooled reference. If not, it registers the current string in the pool and returns it.
Memory Evolution: PermGen vs. Java Heap
| Java Version | Memory Region | GC & OOM Behavior |
|---|---|---|
| Java 6 & Earlier | PermGen (Permanent Generation) | Fixed size (typically 64–128 MB). Calling intern() frequently caused OutOfMemoryError: PermGen space. Garbage collection in PermGen was rare and stop-the-world. |
| Java 7+ | Standard Java Heap | String Pool moved into the general Heap. Unreferenced interned strings are reclaimed by standard Garbage Collectors during normal cycles. |
| Java 9+ | Heap (Compact Strings) | JEP 254: Backed by byte[] with an encoding-flag (Latin-1 vs UTF-16), reducing string memory footprint by up to 50%. |
🔴 Senior Level
HotSpot Internals: Native C++ StringTable
Under the hood, the String Pool is implemented as a native C++ hash table called StringTable:
- Data Structure: Historically a chained hash table with fixed bucket counts.
- Java 13+ Modernization: Re-implemented as a lock-free
ConcurrentHashTablein native C++, eliminating global lock contention during high-concurrencyString.intern()calls. - Tuning Parameter:
-XX:StringTableSize=N(default: 65,536 buckets on 64-bit JVMs in Java 11/17/21). - The Highload UUID Anti-Pattern: Indiscriminately interning high-cardinality strings (e.g., random transaction UUIDs) overflows hash buckets, degrading
intern()from $O(1)$ to a CPU-exhausting $O(N)$ linked list traversal.
Unified String Deduplication (Java 18+, JDK-8267186)
In modern high-throughput architectures, manually calling intern() is considered an anti-pattern. Instead, enable JVM-managed string deduplication:
-XX:+UseStringDeduplication
- Broad Collector Support: Starting with Java 18, string deduplication is integrated into the unified GC interface and supported by G1, Parallel, Serial, and ZGC.
- How It Operates: A low-priority background GC thread identifies
Stringobjects with identical hashes and matching byte arrays. It modifies one string’s internalvaluefield to point directly to the other’sbyte[]array, freeing the redundant array. - Key Advantage: Operates transparently in the background without application thread latency or native
StringTablehash-table pollution.
4 Tricky Questions
1. How many objects are created by String s = new String("Java")? When does it create 2, 1, or 0 objects?
Answer:
- Standard Scenario (First occurrence of the literal): Exactly 2 objects. Object 1 is the literal
"Java", allocated and registered in the String Pool during class loading. Object 2 is the distinct instance allocated in the standard heap by thenewoperator. - If
"Java"is already present in the String Pool: Exactly 1 object (only the heap instance created vianew; the literal is reused directly from the pool). - If HotSpot C2 applies Escape Analysis and Dead Code Elimination: If
snever escapes the local scope and is never read, C2 can eliminate both allocations entirely via scalar replacement and dead code elimination, allocating 0 objects.
2. Are interned strings in the String Pool permanent GC Roots? Can an interned string be garbage collected?
Answer: Starting with Java 7, strings in the String Pool are NOT unconditionally GC Roots:
- The native
StringTableholds strings via weak references or weak handles (OopStorage). - If a string entered the pool dynamically via
str.intern(), and no active strong references (StrongReference) exist anywhere in the application, the Garbage Collector will reclaim the object from the heap and purge its corresponding entry fromStringTableduring weak reference processing. - However, compile-time string literals embedded in loaded classes are referenced by the class’s
ConstantPooland remain alive as long as their definingClassLoaderis active.
3. What is the JVM architectural difference between "a" + "b" and s1 + s2?
Answer:
"a" + "b": A Compile-Time Constant Expression. The Java compiler (javac) evaluates the concatenation at compile time and emits a single"ab"literal into the class constant pool. At runtime, a singleldc "ab"opcode is executed with zero heap allocation overhead.s1 + s2: Dynamic variable concatenation. In Java 9+, this compiles into aninvokedynamiccall linked toStringConcatFactory.makeConcatWithConstants. At runtime, the JVM computes the combined length, allocates a newbyte[]in the standard heap, and constructs a newStringthat is not placed in the String Pool.
4. If the Garbage Collector deduplicates two strings using -XX:+UseStringDeduplication, does s1 == s2 return true?
Answer:
No, s1 == s2 still evaluates to false!
String deduplication works strictly at the internal array level (byte[] value):
- The
Stringwrapper objects remain distinct heap instances residing at different memory addresses (s1 != s2). - The GC simply repoints the reference:
s2.value = s1.value, allowing the duplicate byte array to be reclaimed. - Logical equality
s1.equals(s2)returnstrue. To achieve reference identity (s1 == s2), explicit interning vias1.intern() == s2.intern()is required.
🎯 Interview Cheat Sheet
Key Takeaways
- Design Pattern: Flyweight pattern for canonicalizing character sequences and conserving memory.
- Immutability Dependency: Immutability is the foundational prerequisite; without it, global sharing creates catastrophic state corruption.
- Memory Location: Java 7+ stores the String Pool in the standard Java Heap (collected by GC); Java 6 and earlier stored it in PermGen.
- C++ StringTable: Managed via a lock-free
ConcurrentHashTable(Java 13+) sized via-XX:StringTableSize. - Modern Alternative: Use
-XX:+UseStringDeduplication(Java 18+ for G1, Parallel, Serial, ZGC) instead of manualintern().
Literal vs. new String() vs. intern()
| Operation | Memory Allocation Location | Placed in String Pool? | == Comparison with Literal |
|---|---|---|---|
String s = "txt"; |
Java Heap (String Pool) | Yes (Automatically) | true |
new String("txt"); |
Java Heap (Outside Pool) | No | false |
str.intern(); |
Canonical Pooled Instance | Yes | true |
Red Flags to Avoid
- ❌ “The String Pool is stored in Metaspace.” (False; Metaspace stores class metadata; the String Pool has lived in the Java Heap since Java 7).
- ❌ “Interned strings can never be garbage collected.” (False; unreferenced interned strings are swept during GC cycles).
- ❌ “String Deduplication makes
s1 == s2true.” (False; only internalbyte[]arrays are unified, object wrapper references remain distinct).
Related Topics
- Why is String Class Immutable — Fundamental reasons for string immutability
- What are the Consequences of String Immutability — Security, performance, and memory impact
- Can You Change String Value via Reflection — Reflection attacks and modern JVM defenses
- Can You Use Immutable Objects as Keys in HashMap — Hash table contract stability