Skip to content
Java strings 6 min read

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 with new 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:

  1. The JVM encounters "hello" in your code.
  2. It checks the String Pool for an existing String object with value "hello".
  3. If found, it returns that reference — no new object is created.
  4. If not found, it creates a new String object 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 final variable initialized with a compile-time constant expression is folded by the compiler. But a final variable 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 VersionDefault 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 MethodLives InPool Entry Created?Use == Safely?
"hello" literalString PoolYes (automatically)Yes (with other literals)
new String("hello")HeapNoNo
new String("hello").intern()Pool (return value)Yes (if not already)Yes (return value only)
Runtime concatenationHeapNoNo

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.

Last updated June 13, 2026
Was this helpful?