String Pool & intern()
Java strings are immutable, and programs tend to create a lot of them — often with identical content. The String Pool is Java’s built-in optimization that avoids storing duplicate string values in memory by reusing a single shared instance for equal literals.
What Is the String Pool?
The String Pool (also called the string intern pool or string constant pool) is a special region inside the JVM heap where string literals are stored and reused. When you write a string literal in your source code, the JVM checks whether that value already exists in the pool. If it does, it returns the existing reference instead of allocating a new object.
Note: Before Java 7, the String Pool lived in the PermGen space (a separate, fixed-size memory region). Starting with Java 7, it was moved to the main heap, which means it benefits from garbage collection and is no longer capped by
-XX:MaxPermSize.
Literals vs. new String()
The key distinction is how you create a string:
String a = "hello"; // goes into the String Pool
String b = "hello"; // reuses the same pool entry
String c = new String("hello"); // always creates a new heap object
System.out.println(a == b); // true — same pool reference
System.out.println(a == c); // false — c is on the heap, outside the pool
System.out.println(a.equals(c)); // true — same content
Output:
true
false
true
== compares references (memory addresses), while .equals() compares content. This is why you should always use .equals() to compare strings — not ==.
Warning: Relying on
==for string comparison is a classic bug. Two strings can have identical content but different references if one was created withnew String(...)or built at runtime.
How the Pool Works Step by Step
When the JVM loads your class, it processes the constant pool embedded in the .class bytecode file. Each unique string literal becomes an entry. At runtime:
- The JVM encounters
"hello"in your code. - It checks the String Pool for an existing
Stringobject with value"hello". - If found, it returns that reference — no new object is created.
- If not found, it creates a new
Stringobject in the pool and returns its reference.
This lookup is backed by a hash table inside the JVM, so lookups are O(1) on average.
The intern() Method
String.intern() lets you manually add a runtime-created string to the pool (or retrieve the existing pool entry if the value is already there):
String x = new String("world"); // on the heap, not in the pool
String y = x.intern(); // now y points to the pool entry
String z = "world"; // already in the pool
System.out.println(y == z); // true — same pool reference
System.out.println(x == z); // false — x is still the heap object
Output:
true
false
After calling intern(), the returned reference points to the pool version. The original heap object (x) is unaffected.
When Should You Call intern()?
intern() is useful in specific high-performance scenarios where you know you will compare or store a huge number of strings with many duplicates:
- Parsers or compilers that process repeated identifiers
- Large data pipelines where string keys repeat millions of times
- Reducing memory footprint when loading data from files or databases
// Example: interning strings read from a large CSV file
String[] rows = readMillionsOfRows(); // many duplicate city names
for (int i = 0; i < rows.length; i++) {
rows[i] = rows[i].intern(); // deduplicate in the pool
}
Tip: In most everyday code you do NOT need
intern(). Overusing it can actually hurt performance by bloating the pool with strings that are barely reused. Profile first.
Compile-Time Constant Folding and the Pool
The Java compiler is smart enough to fold constant string expressions at compile time. This means "foo" + "bar" is treated exactly like "foobar" — a single pool entry:
String s1 = "foo" + "bar"; // compiler folds to "foobar" at compile time
String s2 = "foobar";
System.out.println(s1 == s2); // true — same pool entry!
Output:
true
However, if either operand is a variable (even a final one computed at runtime), the JVM cannot fold it, and the result lands on the heap:
String part = "foo";
String s3 = part + "bar"; // runtime concatenation — new heap object
System.out.println(s2 == s3); // false
Output:
false
Note: A
finalvariable initialized with a compile-time constant expression is folded by the compiler. But afinalvariable set at runtime (e.g., from a method call) is not.
Under the Hood
The String Pool is implemented as a hash table maintained by the JVM’s StringTable. Its default size has grown over Java versions:
| Java Version | Default StringTable Buckets |
|---|---|
| Java 6 and earlier | ~1,009 |
| Java 7 / 8 | ~60,013 |
| Java 11+ | ~65,536 |
You can tune it with -XX:StringTableSize=<n> (use a prime number for best distribution).
Object Layout in Memory
A pooled string and a heap string contain the same internal data — a char[] (Java 8 and earlier) or a byte[] plus an encoding flag (Java 9+, Compact Strings). The only difference is where the String object lives:
- Pool string — inside the
StringTable, kept alive as long as the pool entry exists - Heap string — a regular object subject to normal garbage collection
Because pool entries are referenced by the StringTable (a GC root), they are only collected when the pool entry itself is removed — which does happen in modern JVMs when strings are no longer referenced from anywhere else.
G1 String Deduplication (Java 8u20+)
The G1 garbage collector introduced String Deduplication (-XX:+UseStringDeduplication). Unlike interning, this operates at the GC level: it finds String objects on the heap that have the same character content and makes them share a single backing byte[] array — without touching object references. This saves memory without requiring any code changes, but only works with G1.
Quick Reference: Literal vs. new vs. intern()
| Creation Method | Lives In | Pool Entry Created? | Use == Safely? |
|---|---|---|---|
"hello" literal | String Pool | Yes (automatically) | Yes (with other literals) |
new String("hello") | Heap | No | No |
new String("hello").intern() | Pool (return value) | Yes (if not already) | Yes (return value only) |
| Runtime concatenation | Heap | No | No |
intern() Performance Caution
Each call to intern() acquires a lock on the StringTable — it is a native synchronized operation. In highly concurrent code with millions of intern() calls, this can become a bottleneck. If you’re doing bulk deduplication, benchmark your specific workload before committing to intern() across the board.
// Benchmarking interning impact (pseudocode sketch)
long start = System.nanoTime();
for (String s : hugeList) {
s.intern(); // watch this cost under concurrency
}
long elapsed = System.nanoTime() - start;
System.out.println("Interning took: " + elapsed + " ns");
Understanding why String is immutable is the foundation for understanding why the pool is safe — two threads can share the same pool reference without any synchronization because neither can modify the content.
Related Topics
- Why String is Immutable — the property that makes sharing pool references safe
- String Comparison — when to use
==vs.equals()vscompareTo() - Strings — overview of the String class and how it works
- JVM Architecture — understand the heap regions where the pool lives
- Garbage Collection Deep-Dive — how the JVM reclaims string objects and pool entries
- StringBuilder — build strings at runtime without creating pool pollution