How String Pool Works
The String Pool (also referred to as the String Intern Pool) is a dedicated storage and deduplication structure maintained by the Java Virtual Machine (JVM) designed to optimize...
🟢 Junior Level
The String Pool (also referred to as the String Intern Pool) is a dedicated storage and deduplication structure maintained by the Java Virtual Machine (JVM) designed to optimize memory utilization by sharing immutable String instances.
The Essence in 30 Seconds
Because strings in Java are immutable, storing thousands of identical strings (such as "USD", "ACTIVE", or "SUCCESS") throughout the heap is redundant and memory-inefficient:
- When declaring a string using a string literal (
String s = "Hello";), the JVM first checks whether an identical string already exists within the String Pool. - If the string exists in the pool, the JVM does not allocate a new object; it simply returns a reference to the existing instance.
- If the string is not present in the pool, a new
Stringobject is allocated on the Java Heap, registered in the pool, and its reference is returned.
// String Pool mechanics demonstration:
String s1 = "Java";
String s2 = "Java";
// s1 and s2 point to the EXACT SAME physical object in memory:
System.out.println(s1 == s2); // true (reference equality!)
System.out.println(s1.equals(s2)); // true (value equality)
// Explicit allocation via 'new' bypasses direct pool reuse:
String s3 = new String("Java");
System.out.println(s1 == s3); // false (different heap instances!)
System.out.println(s1.equals(s3)); // true (identical character sequence)
Why the String Pool Matters
In typical enterprise Java applications, String objects account for 25% to 40% of all heap memory allocations. The String Pool drastically reduces an application’s memory footprint by preventing the creation of hundreds of thousands of redundant duplicate character arrays.
🟡 Middle Level
Internal Architecture: The HotSpot StringTable
At the HotSpot JVM level, the String Pool is implemented in native C++ as a hash table called StringTable:
- The entries in the table store pointers (
oop, Ordinary Object Pointer) tojava.lang.Stringobjects residing in the Java Heap. StringTableresolves lookups using a hash computed from the string’s characters.
flowchart LR
subgraph HotSpotNative["HotSpot Native Memory (C++ StringTable)"]
Bucket0["bucket[0]: [hash: 2301548 | oop: 0x7FA01]"]
Bucket1["bucket[1]: NULL"]
Bucket2["bucket[2]: [hash: 7120391 | oop: 0x7FB20]"]
end
subgraph JavaHeap["Java Heap"]
StrObj["String Object (0x7FA01)\nvalue: byte[] {'J','a','v','a'}\ncoder: LATIN1 (0)"]
end
Bucket0 --> StrObj
Two Paths into the String Pool
| Ingress Mechanism | Execution Timing | JVM Mechanism |
|---|---|---|
String Literals ("text") |
Class Loading & Linking | The javac compiler emits string constants into the .class file’s Constant_Utf8_info table within the bytecode Constant Pool. Upon class loading, the JVM resolves these constants via the ldc (load constant) bytecode instruction, registering references into the StringTable. |
Manual Interning (str.intern()) |
Dynamic Runtime | The native method public native String intern() queries StringTable: if a string with identical content exists, the pooled canonical reference is returned; otherwise, the calling String reference is registered into StringTable and returned. |
// Dynamic runtime interning:
String dynamicString = new StringBuilder("Ja").append("va").toString(); // Plain Heap object (outside pool)
String internedString = dynamicString.intern(); // Registered or retrieved from pool
String literalString = "Java"; // Retrieved from pool
System.out.println(internedString == literalString); // true!
🔴 Senior Level
Memory Area Evolution Across JVM Versions
The physical memory placement of the String Pool has undergone fundamental architectural changes:
| Java Version | Memory Area | Characteristics & Operational Hazards |
|---|---|---|
| Java 6 and earlier | PermGen (Permanent Generation) | Fixed size configured via -XX:MaxPermSize. PermGen garbage collection was infrequent and expensive. Heavy runtime interning frequently crashed the JVM with java.lang.OutOfMemoryError: PermGen space. |
| Java 7 | Java Heap (Old / Tenured Gen) | The String Pool was relocated to the main Java Heap. Pooled strings became subject to standard Garbage Collection cycles and bounded by standard -Xmx heap constraints. |
| Java 8+ to Java 21+ | Java Heap (NOT Metaspace!) | Critical Interview Trap: When PermGen was removed in Java 8, class metadata moved to off-heap native memory (Metaspace). However, the String Pool remained firmly in the Java Heap. It is never stored in Metaspace. |
StringTable Evolution: From Fixed Buckets to ConcurrentHashTable
Prior to Java 11, HotSpot’s StringTable was implemented as a fixed-size bucket array with collision chaining (singly linked lists):
- Bucket capacity was configured via
-XX:StringTableSize=N(default was $1,009$ in Java 6, and $60,013$ in Java 7u40 through Java 8). - The Scalability Bottleneck: Pre-Java 11
StringTablecould not resize dynamically. If an application interned several million strings into a 60,013-bucket table, collision chains averaged ~80 elements per bucket. Lookup complexity degraded from $O(1)$ to $O(N)$, causing severe CPU spikes duringString.intern()calls.
The Java 11 Breakthrough (JEP 341):
In Java 11, the native StringTable was re-engineered using a scalable ConcurrentHashTable:
- Dynamic Resizing: Supports lock-free concurrent resizing when the load factor exceeds thresholds.
- Lock-Free Reads: Parallel worker threads query the table concurrently without global spinlock contention.
- GC Integration: Cooperates with Safepoints and concurrent marking phases for clean unreferenced entry purging.
Garbage Collection Lifecycle of Pooled Strings
Strings stored in the String Pool are not immortal (starting from Java 7+):
- Dynamic Interns: If a string is added to the pool via
s.intern(), and no active Strong References remain reachable from any GC Root, the Garbage Collector reclaims the string object during GC cycles and evicts the corresponding native entry fromStringTable. - Class Literals: Literals embedded in compiled classes remain reachable as long as their defining
ClassLoaderremains alive. For standard applications, system-loaded literals persist for the lifetime of the JVM.
Modern Alternative: G1 String Deduplication
In modern enterprise architectures, calling String.intern() manually is considered an anti-pattern due to CPU overhead and table contention. Modern JVMs (G1, Shenandoah, ZGC) offer transparent, asynchronous string deduplication:
-XX:+UseStringDeduplication -XX:StringDeduplicationAgeThreshold=3
- How It Works: During concurrent background GC phases, the collector inspects
Stringcandidates that survive beyond the age threshold. - If two strings contain identical characters, the GC does not merge the
Stringobjects themselves (s1 != s2), but rewires the internal backingbyte[] valuearray of the second string to point to the first string’s array. - The redundant byte array is collected, cutting string heap footprint by 10%–25% in production without modifying application code or stressing
StringTable.
4 Tricky Interview Questions
1. Can calling String.intern() cause an OutOfMemoryError, and in which memory area does it occur on Java 17/21?
Answer:
Yes, it can. On modern JVMs (Java 8, 17, 21+), it produces java.lang.OutOfMemoryError: Java heap space.
Since Java 7, the String Pool resides inside the standard Java Heap (not in PermGen or Metaspace). If an application continuously generates unique dynamic strings (e.g., UUID.randomUUID().toString().intern()) and retains references to them in long-lived data structures, the heap fills up, the GC cannot collect them, and standard heap exhaustion occurs.
2. What is the evaluation result of s1 == s2 in the following code, and what happens under the hood:
String s1 = new String("hello").intern();
String s2 = "hello";
Answer:
The expression evaluates to true.
Under the hood:
- During class loading, the string literal
"hello"is compiled into the constant pool and registered in theStringTable. new String("hello")allocates a new distinctStringinstance on the Java Heap containing a copy of"hello".- Invoking
.intern()on that new instance looks up"hello"in theStringTable, finds the pre-existing literal entry, and returns the reference to that pooled canonical instance. Thus,s1points to the pooled literal. String s2 = "hello"resolves directly to the exact same pooled literal reference.- Because
s1ands2hold identical memory addresses, reference comparison==evaluates totrue.
3. Are strings inside the String Pool collected by the Garbage Collector if there are no live references to them?
Answer:
Yes, they are collected.
Starting with Java 7, the String Pool is in the Java Heap. The native HotSpot StringTable holds weak references (OopStorage in modern OpenJDK) to pooled strings. If a string was placed into the pool via intern() and no thread stacks, static fields, or live objects hold strong references to it, the Garbage Collector will reclaim the object and clear its corresponding entry from StringTable during collection phases.
4. What is the fundamental difference between memory optimization via String.intern() and the JVM flag -XX:+UseStringDeduplication?
Answer:
String.intern():- Operates synchronously in application threads.
- Unifies the
java.lang.Stringobject instances (references become equal:s1 == s2). - Imposes runtime CPU overhead on every invocation due to native
StringTablehash lookups and concurrency locks.
-XX:+UseStringDeduplication(G1 / Shenandoah / ZGC):- Operates asynchronously and transparently in background GC threads.
- Does NOT unify
Stringinstances (references remain distinct:s1 != s2, buts1.equals(s2) == true). - De-duplicates only the internal backing
byte[] valuearray, freeing memory without touching application code orStringTable.
🎯 Interview Cheat Sheet
30-Second Summary
“The String Pool is a specialized cache in the Java Heap, implemented in HotSpot C++ as
StringTable. It eliminates string duplicates by sharing immutable instances. String literals enter the pool automatically during class loading vialdcbytecode instructions, while runtime strings can be pooled manually withString.intern(). Since Java 7, the pool resides in the Heap (not Metaspace) and is garbage-collected when references vanish. In Java 11+,StringTableuses a lock-freeConcurrentHashTablewith dynamic resizing, while G1 GC provides transparent background deduplication via-XX:+UseStringDeduplication.”
Key Concepts Checklist
- Memory Location: Resides in the Java Heap across all modern Java versions (7, 8, 11, 17, 21+). Never in Metaspace.
String.intern()Contract: Returns canonical pooled reference; adds to pool if absent.- Garbage Collection: Unreferenced pooled strings are collected by normal GC cycles.
- JVM Architecture: HotSpot
StringTableC++ hash table (ConcurrentHashTablesince Java 11). - G1 Deduplication:
-XX:+UseStringDeduplicationunifies backingbyte[]arrays in background GC threads without changing string references.
Common Pitfalls & Red Flags
- ❌ “The String Pool was moved to Metaspace in Java 8.” (Fatal blunder: Metaspace stores class metadata; the String Pool is in the Java Heap).
- ❌ “Objects inside the String Pool are permanent and can never be garbage collected.” (False: dynamically interned strings without strong roots are collected by the GC).
- ❌ “Calling
String.intern()has zero performance cost.” (False: it performs native hash table queries that can cause severe CPU bottlenecks under high concurrency).
Related Topics
- Difference Between Creating String via Literal and via new — Allocation mechanics
- When to Use intern() — Practical tradeoffs
- Where is String Pool Stored (Which Memory Area) — Deep memory layout
- Can String Pool Cause OutOfMemoryError — OOM scenarios
- What is String Deduplication in G1 GC — Background array sharing