Understanding Java String Constant Pool and the Behavior of String.intern() Across JDK Versions
This article explains the Java constant pool concept, details how the String constant pool works in different JDK versions, demonstrates the effects of String.intern() with code examples, and discusses practical applications and pitfalls of using intern for memory optimization and synchronization.
0. Background
Java defines eight primitive types and a special reference type String . To improve runtime speed and reduce memory usage, the JVM provides a constant pool, which acts as a system‑level cache.
All primitive type constant pools are coordinated by the system, while the String constant pool has two main usage patterns:
String literals declared with double quotes are stored directly in the constant pool.
Non‑literal strings can be added to the pool via String.intern() , which checks the pool for an existing entry and inserts the string if absent.
1. Constant Pool
1.1 What is the constant pool?
The JVM constant pool consists of the class‑file constant pool, the runtime constant pool, the global string constant pool, and the primitive wrapper object pool.
1.1.0 Method Area
The method area stores class structure information; after an object is created, its type information resides in the method area, while instance data lives on the heap.
Memory reclamation in this area mainly targets constant‑pool cleanup and unloading of other data, and its reclamation rate is generally lower than that of the heap.
1.1.1 Class‑file Constant Pool
A .class file is a binary stream generated during compilation; it contains a class‑file constant pool.
The class‑file constant pool stores two kinds of constants: literals and symbolic references.
Literal examples: Text strings, e.g., public String s = "abc"; Final members (static, instance, or local), e.g., public final static int f = 0x101;, final int temp = 3; For primitive fields such as int value = 1 , only the descriptor and name are kept; the literal value itself is not stored in the pool.
Symbolic references include: Fully qualified class/interface names, e.g., java/lang/String . Field names and descriptors. Method names and descriptors (parameter types + return type).
1.1.2 Runtime Constant Pool
When a class is loaded, its class‑file constant pool is copied into the runtime constant pool, which resides in the method area. During the resolution phase, symbolic references are replaced by direct references, and the string constant pool ( StringTable ) is consulted to ensure consistency.
The runtime constant pool is dynamic: new constants can be generated at runtime, most commonly via String.intern() .
1.1.3 String Constant Pool
In JDK 6 and earlier the string pool lived in the method area; from JDK 7 onward it was moved to the heap because the method‑area space was too limited.
HotSpot implements the pool with a StringTable hash table (default size 1009). Each VM instance has a single shared StringTable . In JDK 6 the table length is fixed, leading to hash collisions and long lookup chains when many strings are interned, which degrades performance. JDK 7 allows the table size to be configured.
Design rationale:
String allocation is costly; frequent creation of identical strings harms performance.
The JVM caches strings in a pool, reusing existing instances when possible.
Because strings are immutable, they can be safely shared without data races.
Interned strings are not reclaimed by the garbage collector while the pool exists.
2. String.intern() and the String Constant Pool
/**
* Returns a canonical representation for the string object.
*
* When the intern method is invoked, if the pool already contains a
* string equal to this String object as determined by #equals(Object),
* then the string from the pool is returned. Otherwise, this String
* object is added to the pool and a reference to this String object
* is returned.
*/
public native String intern();The location of the string pool changes with the JDK version: in JDK 6 it resides in the permanent generation (method area), in JDK 7 it moves to the heap, and in JDK 8 the permanent generation is replaced by Metaspace.
Example test code demonstrates different outputs on JDK 6 vs JDK 7:
@Test
public void test() {
String s = new String("2");
s.intern();
String s2 = "2";
System.out.println(s == s2);
String s3 = new String("3") + new String("3");
s3.intern();
String s4 = "33";
System.out.println(s3 == s4);
}
// JDK6 output
false
false
// JDK7 output
false
trueExplanation for JDK 6:
String s = new String("2") creates a heap object and a literal "2" in the pool.
s.intern() finds the existing literal and returns its reference.
String s2 = "2" reuses the literal, so s == s2 is false.
For the concatenated string "33", s3.intern() creates a new pool entry, but the later literal "33" refers to a different object, so s3 == s4 is false.
Explanation for JDK 7:
Both the literal "2" and the heap object are stored, but s.intern() still returns the pool reference, so s == s2 remains false.
When s3.intern() is called, the pool does not yet contain "33", so the reference of the newly created heap string is stored in the pool.
The subsequent literal "33" resolves to the same heap object, making s3 == s4 true.
3. Applications of String.intern()
1) Memory saving : In workloads that create massive numbers of temporary strings, interning reduces heap usage dramatically. A benchmark shows that without interning 10 million strings consume ~640 MB, while interning reduces the footprint to ~133 KB.
static final int MAX = 1000 * 10000;
static final String[] arr = new String[MAX];
public static void main(String[] args) throws Exception {
Integer[] DB_DATA = new Integer[10];
Random random = new Random(10 * 10000);
for (int i = 0; i < DB_DATA.length; i++) {
DB_DATA[i] = random.nextInt();
}
long t = System.currentTimeMillis();
for (int i = 0; i < MAX; i++) {
// arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length]));
arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();
}
System.out.println((System.currentTimeMillis() - t) + "ms");
System.gc();
}2) Locking via unique interned strings : Because interned strings are unique, they can serve as lightweight monitor objects for synchronizing access to a shared resource (e.g., using a city name as a lock).
3) Misuse warning : Libraries such as FastJSON intern every JSON key. While this speeds up parsing for static keys, it can overload the string pool if keys are highly dynamic, leading to memory pressure.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.