Java Tutorial
🔍

Java String Pool

Java String Pool

Every time you write String name = "Priya" in Java, something interesting happens behind the scenes. The JVM does not immediately create a new object. It first checks a special memory area called the String Pool (also called the String Constant Pool or String Intern Pool). If "Priya" already exists there, the JVM reuses the same object — it does not create a duplicate. Only if it does not exist does it create a new entry.

This is one of Java's most impactful memory optimisations. It works silently, affects how == behaves with strings, explains why strings are immutable, and is tested in nearly every Java interview. Understanding it changes how you think about every string in your program.

Where the String Pool Lives

The String Pool is a region inside the Java Heap memory. In Java 7 and earlier, it lived in a special area called PermGen (Permanent Generation) — separate from the main heap. Starting from Java 8, the pool was moved into the main heap. This was an important change because PermGen had a fixed size and could cause OutOfMemoryError: PermGen space in applications that generated many unique strings.

Java 8 and later — JVM Memory Layout:

  ┌────────────────────────────────────────────────────────┐
  │                      JVM HEAP                         │
  │                                                        │
  │   ┌──────────────────────────────────────────────┐    │
  │   │              String Pool                     │    │
  │   │  "hello"  "world"  "Priya"  "admin"  "Java"  │    │
  │   │  (String literals and interned strings)      │    │
  │   └──────────────────────────────────────────────┘    │
  │                                                        │
  │   Regular Heap (objects created with new):             │
  │   ┌──────┐  ┌──────┐  ┌───────┐  ┌───────┐           │
  │   │"hi"  │  │"hi"  │  │"Java" │  │Order  │           │
  │   └──────┘  └──────┘  └───────┘  └───────┘           │
  │      ▲         ▲          ▲                            │
  │    s3          s4        s5 (new String — not pooled) │
  └────────────────────────────────────────────────────────┘

  Stack (per thread):
  ┌──────────────────────────────────────────────────────┐
  │  s1 → [address of "hello" in pool]                  │
  │  s2 → [address of "hello" in pool]  (same address!) │
  │  s3 → [address of "hi" on heap]                     │
  └──────────────────────────────────────────────────────┘

In Java 8+, having the pool in the main heap means it can grow dynamically and is managed by the garbage collector like any other object — much more flexible than the fixed-size PermGen.

String Literals vs new String()

The key to understanding the pool is knowing the two ways to create a String and how they differ in memory.

// File: LiteralVsNewDemo.java public class LiteralVsNewDemo { public static void main(String[] args) { // Way 1 — String literal // JVM checks the pool first. // "Java" does not exist yet → creates it in the pool String s1 = "Java"; // "Java" already exists in the pool → reuses the same object String s2 = "Java"; // Way 2 — new String() // Always creates a new object in the regular heap // Bypasses the pool entirely String s3 = new String("Java"); String s4 = new String("Java"); System.out.println("=== Reference Comparison (==) ==="); System.out.println("s1 == s2 : " + (s1 == s2)); // true — same pool object System.out.println("s1 == s3 : " + (s1 == s3)); // false — pool vs heap System.out.println("s3 == s4 : " + (s3 == s4)); // false — two separate heap objects System.out.println("s2 == s4 : " + (s2 == s4)); // false — pool vs heap System.out.println(); System.out.println("=== Content Comparison (equals) ==="); System.out.println("s1.equals(s2) : " + s1.equals(s2)); // true System.out.println("s1.equals(s3) : " + s1.equals(s3)); // true System.out.println("s3.equals(s4) : " + s3.equals(s4)); // true System.out.println(); // Identity hash code confirms which objects are the same System.out.println("=== Identity Hash Codes ==="); System.out.println("s1 id: " + System.identityHashCode(s1)); System.out.println("s2 id: " + System.identityHashCode(s2)); // same as s1 System.out.println("s3 id: " + System.identityHashCode(s3)); // different System.out.println("s4 id: " + System.identityHashCode(s4)); // different from s3 } }
Output:
=== Reference Comparison (==) ===
s1 == s2 : true
s1 == s3 : false
s3 == s4 : false
s2 == s4 : false

=== Content Comparison (equals) ===
s1.equals(s2) : true
s1.equals(s3) : true
s3.equals(s4) : true

=== Identity Hash Codes ===
s1 id: 1173230247
s2 id: 1173230247
s3 id: 856419764
s4 id: 1311053135

s1 and s2 share the same identity hash code — they are literally the same object. s3 and s4 each have a unique address — separate objects on the heap even though their content is identical.

How the Pool Works — Step by Step

Execution trace when these four lines run:

Line 1: String city1 = "Mumbai";
  → JVM scans the pool: "Mumbai" NOT found
  → Creates "Mumbai" in the pool
  → city1 points to the pool entry

Line 2: String city2 = "Mumbai";
  → JVM scans the pool: "Mumbai" FOUND
  → Returns the existing pool entry
  → city2 points to the SAME pool entry as city1

Line 3: String city3 = new String("Mumbai");
  → BYPASSES the pool entirely
  → Creates a new String object on the regular heap
  → city3 points to this new heap object

Line 4: String city4 = city3.intern();
  → JVM scans the pool: "Mumbai" FOUND (from Line 1)
  → Returns the existing pool entry
  → city4 points to the SAME object as city1 and city2

Final state:
  Pool:   [ "Mumbai" ] ◄── city1, city2, city4
  Heap:   [ "Mumbai" ] ◄── city3 (separate object)
// File: PoolStepByStepDemo.java public class PoolStepByStepDemo { public static void main(String[] args) { String city1 = "Mumbai"; // created in pool String city2 = "Mumbai"; // reuses pool entry String city3 = new String("Mumbai"); // new heap object String city4 = city3.intern(); // returns pool entry System.out.println("city1 == city2 : " + (city1 == city2)); // true — same pool obj System.out.println("city1 == city3 : " + (city1 == city3)); // false — pool vs heap System.out.println("city1 == city4 : " + (city1 == city4)); // true — intern returns pool obj System.out.println("city3 == city4 : " + (city3 == city4)); // false — heap vs pool System.out.println(); System.out.println("All have same content:"); System.out.println("city1.equals(city3) : " + city1.equals(city3)); // true System.out.println("city3.equals(city4) : " + city3.equals(city4)); // true } }
Output:
city1 == city2 : true
city1 == city3 : false
city1 == city4 : true
city3 == city4 : false

All have same content:
city1.equals(city3) : true
city3.equals(city4) : true

intern() is the bridge between the heap and the pool. It takes a String that is on the heap and returns its pooled equivalent — creating the pool entry if it does not exist.

The intern() Method — When and Why to Use It

intern() returns the canonical pooled version of a String. If the pool already has a String with the same content, that pooled reference is returned. If not, the string is added to the pool and that reference is returned.

// File: InternDemo.java public class InternDemo { public static void main(String[] args) { // Scenario 1 — intern() returns the pool reference String heapStr = new String("developer"); String poolStr = heapStr.intern(); // returns pool reference String literal = "developer"; // already in pool System.out.println("=== intern() basics ==="); System.out.println("heapStr == poolStr : " + (heapStr == poolStr)); // false — heap vs pool System.out.println("poolStr == literal : " + (poolStr == literal)); // true — same pool obj System.out.println("heapStr == literal : " + (heapStr == literal)); // false System.out.println(); // Scenario 2 — intern() of a new string not yet in pool String built1 = new String("freshtech"); String built2 = new String("freshtech"); System.out.println("=== intern() of new strings ==="); System.out.println("built1 == built2 : " + (built1 == built2)); // false System.out.println("built1.intern() == built2.intern(): " + (built1.intern() == built2.intern())); // true — both intern to same pool entry System.out.println(); // Scenario 3 — performance use case // In systems that process millions of repeated strings (like log levels, status codes) // internment avoids holding duplicate objects in memory String[] statuses = { new String("SUCCESS"), new String("FAILED"), new String("SUCCESS"), new String("PENDING"), new String("SUCCESS"), new String("FAILED"), }; System.out.println("=== Before intern — each is a separate object ==="); System.out.println("statuses[0] == statuses[2] : " + (statuses[0] == statuses[2])); // false // Intern all — now identical strings share the same object for (int i = 0; i < statuses.length; i++) { statuses[i] = statuses[i].intern(); } System.out.println("After intern — identical strings share one object:"); System.out.println("statuses[0] == statuses[2] : " + (statuses[0] == statuses[2])); // true System.out.println("statuses[1] == statuses[5] : " + (statuses[1] == statuses[5])); // true System.out.println("statuses[0] == statuses[1] : " + (statuses[0] == statuses[1])); // false } }
Output:
=== intern() basics ===
heapStr == poolStr  : false
poolStr == literal  : true
heapStr == literal  : false

=== intern() of new strings ===
built1 == built2              : false
built1.intern() == built2.intern(): true

=== Before intern — each is a separate object ===
statuses[0] == statuses[2] : false

After intern — identical strings share one object:
statuses[0] == statuses[2] : true
statuses[1] == statuses[5] : true
statuses[0] == statuses[1] : false

In most application code, intern() is not needed — use equals() for comparison. intern() is useful in memory-intensive systems that process millions of repeated strings — log aggregators, data pipelines, configuration parsers — where deduplication into the pool significantly reduces heap pressure.

Compile-Time Constants and the Pool

Java performs string concatenation at compile time when all parts are compile-time constants (literals or final variables with literal values). The result is treated as a literal and placed in the pool.

// File: CompileTimeDemo.java public class CompileTimeDemo { public static void main(String[] args) { // Compile-time constants — compiler folds at compile time String s1 = "hello"; String s2 = "hel" + "lo"; // compile-time concat → same as "hello" String s3 = "hel" + "lo" + " world"; // compile-time concat final String prefix = "hel"; // final with literal — compile-time constant String s4 = prefix + "lo"; // compile-time concat — final + literal System.out.println("=== Compile-time constants ==="); System.out.println("s1 == s2 : " + (s1 == s2)); // true — both "hello" in pool System.out.println("s1 == s4 : " + (s1 == s4)); // true — final + literal = compile-time System.out.println(); // Runtime concatenation — creates a new heap object String base = "hel"; // NOT final — runtime variable String s5 = base + "lo"; // runtime concat — new heap object String part1 = "hel"; // final but NOT declared final // Actually let's use a method call which is always runtime String s6 = "hel".concat("lo"); // method call — always runtime System.out.println("=== Runtime concatenation ==="); System.out.println("s1 == s5 : " + (s1 == s5)); // false — s5 on heap System.out.println("s1 == s6 : " + (s1 == s6)); // false — s6 on heap System.out.println(); System.out.println("Content is still equal:"); System.out.println("s1.equals(s5) : " + s1.equals(s5)); // true System.out.println("s1.equals(s6) : " + s1.equals(s6)); // true System.out.println(); // Final variable with literal value = compile-time constant final String A = "hello"; final String B = "world"; String AB = A + " " + B; // compile-time concat (all finals + literal) String ab = "hello" + " " + "world"; // also compile-time System.out.println("=== final variables ==="); System.out.println("AB == ab : " + (AB == ab)); // true — both in pool System.out.println("AB == 'hello world': " + (AB == "hello world")); // true } }
Output:
=== Compile-time constants ===
s1 == s2 : true
s1 == s4 : true

=== Runtime concatenation ===
s1 == s5 : false
s1 == s6 : false

Content is still equal:
s1.equals(s5) : true
s1.equals(s6) : true

=== final variables ===
AB == ab         : true
AB == 'hello world': true

This is why final String prefix = "hel"; prefix + "lo" produces a pooled string, but String prefix = "hel"; prefix + "lo" produces a heap string. The compiler can evaluate the first at compile time — the second could theoretically change at runtime even though it does not in practice.

Why String Immutability and the Pool Are Inseparable

The String pool can only work because strings are immutable. If you could change the characters of a pooled String, every variable pointing to that pool entry would silently see a different value. Immutability is what makes safe sharing possible.

// File: ImmutabilityPoolDemo.java public class ImmutabilityPoolDemo { public static void main(String[] args) { String city1 = "Delhi"; String city2 = "Delhi"; // same pool object as city1 System.out.println("city1 == city2 : " + (city1 == city2)); // true — same object System.out.println("city1 : " + city1); System.out.println("city2 : " + city2); System.out.println(); // Strings are immutable — you cannot change the characters // city1.charAt(0) = 'B'; // would not compile — no such method // The only thing you can do is make city1 point to a NEW string city1 = city1.replace("Delhi", "Bengaluru"); // replace() returned a NEW String — city1 now points to "Bengaluru" // city2 still points to "Delhi" in the pool — UNCHANGED System.out.println("After city1 = city1.replace('Delhi', 'Bengaluru'):"); System.out.println("city1 : " + city1); // Bengaluru System.out.println("city2 : " + city2); // Delhi — untouched System.out.println(); // Why this matters — thread safety // Multiple threads can hold references to the same pooled String // None of them can modify the shared string → no synchronisation needed System.out.println("=== Thread Safety Benefit ==="); System.out.println("Both threads can read 'Delhi' safely:"); System.out.println("Thread 1 sees: " + city2); System.out.println("Thread 2 sees: " + city2); // always "Delhi" — cannot change System.out.println(); // Why == sometimes seems to work for strings — the pool creates this illusion System.out.println("=== The == Illusion From the Pool ==="); String role1 = "admin"; // pool String role2 = "admin"; // pool — reuses same object System.out.println("role1 == role2 : " + (role1 == role2)); // true — LUCKY (pool) // But this breaks when strings come from outside the pool String userInput = getUserRole(); // simulates real-world — returns heap String System.out.println("userInput == 'admin' : " + (userInput == "admin")); // false — BREAKS System.out.println("'admin'.equals(userInput): " + "admin".equals(userInput)); // true — CORRECT } static String getUserRole() { // In real systems, strings come from databases, APIs, user forms // These are always new heap objects — never pool objects return new String("admin"); } }
Output:
city1 == city2 : true
city1 : Delhi
city2 : Delhi

After city1 = city1.replace('Delhi', 'Bengaluru'):
city1 : Bengaluru
city2 : Delhi

=== Thread Safety Benefit ===
Both threads can read 'Delhi' safely:
Thread 1 sees: Delhi
Thread 2 sees: Delhi

=== The == Illusion From the Pool ===
role1 == role2 : true
userInput == 'admin' : false
'admin'.equals(userInput): true

city1 = city1.replace(...) creates a new String and makes city1 point to it. The pool's "Delhi" entry is unchanged. city2 still points to it — untouched. This is immutability protecting every other reference to the same pooled string.

Literal vs new String() vs intern() — Comparison Table

AspectString s = "hello"String s = new String("hello")s.intern()
Memory locationString PoolRegular HeapString Pool (adds if absent, returns existing)
Checks pool firstYesNo — always creates newYes
Creates new objectOnly if not in poolAlwaysNever — returns existing or adds
== with same literaltruefalsetrue
equals() with same contenttruetruetrue
Garbage collectibleNo — pool entries are GC rootsYes — if no referencesDepends on Java version
When to useAlmost alwaysRarely — when explicit heap object neededMemory-intensive systems with repeated strings
PerformanceFast — pool lookupSlightly slower — heap allocationAdds overhead — use only when justified
Java version behaviourSame across all versionsSameJava 6 and earlier: PermGen; Java 7+: main heap

Basic Example

public class Main { public static void main(String[] args) { String a = "cat"; // literal — goes into the pool String b = "cat"; // same literal — reuses the pool entry String c = new String("cat"); // new keyword — always a new heap object String d = c.intern(); // returns the pool's "cat" reference System.out.println("a == b : " + (a == b)); System.out.println("a == c : " + (a == c)); System.out.println("a == d : " + (a == d)); } }

Output

a == b : true
a == c : false
a == d : true

How it works:

  1. ›a and b are both string literals with the same text, so the JVM reuses the same pool entry for both — a == b is true.
  2. ›c is created with new String(...), which always creates a separate object on the regular heap, bypassing the pool — so a == c is false even though the content matches.
  3. ›c.intern() looks up "cat" in the pool and returns the existing pooled reference — the same one a and b already point to.
  4. ›a == d is true because d now points to that same pooled object.

Best Practices

Always use String literals for known constant strings — not new String(). Literals benefit from pool sharing automatically. new String("PLACED") always wastes a heap object. For configuration values, status codes, role names, and fixed constants, use literals directly or static final string fields — both land in the pool.

Always compare String content with equals() — never rely on the pool for ==. The pool only applies to literals and explicitly interned strings. Strings from databases, APIs, user input, and file reading are always heap objects. Code that accidentally works with == because the pool makes some comparisons true is a ticking bug — the next test with real data will fail.

Use intern() only when the benefit is measurable. intern() adds overhead — it must synchronise access to the shared pool. For typical application strings (usernames, addresses, descriptions), the overhead outweighs any memory benefit. Use it only in systems that demonstrably hold millions of duplicate strings simultaneously — log parsing, data pipeline processing, read-heavy configuration caches.

Declare shared constants as static final String. Constants in a class (public static final String STATUS_ACTIVE = "ACTIVE") are literals — stored in the pool once. All classes referencing OrderStatus.ACTIVE point to the same pool entry. This eliminates the possibility of multiple copies and ensures consistent comparison behaviour.

Common Mistakes

Mistake 1 — Using new String() Where a Literal Would Work

// Unnecessary heap object — always wastes memory String status = new String("PLACED"); // Correct — literal goes into pool, reused on next identical declaration String status = "PLACED";

There is no benefit to new String("PLACED") in normal application code. It creates an extra heap object that equals() would handle correctly anyway, and it prevents pool reuse.

Mistake 2 — Relying on == Because Pool Makes It Work Sometimes

// Appears to work during unit tests with string literals String expected = "SUCCESS"; String actual = "SUCCESS"; // literal — pool object if (actual == expected) { // true — but only because of pool processOrder(); } // Silently fails in production when 'actual' comes from an API call String fromAPI = getStatusFromPaymentGateway(); // returns new String("SUCCESS") if (fromAPI == expected) { // false — heap vs pool — BUG processOrder(); }

Mistake 3 — Mutating a String Reference and Thinking the Pool Entry Changed

String city = "Mumbai"; // Another part of the code also has: String ref = "Mumbai"; // same pool object // You try to "update" city city = "Delhi"; // this does NOT change the pool entry "Mumbai" // it creates "Delhi" in the pool and makes city point to it System.out.println(ref); // still "Mumbai" — pool unchanged, ref unaffected

Mistake 4 — Expecting intern() to Be the Solution for All Comparison Issues

// Interning and using == is NOT a replacement for equals() String s = new String("admin").intern(); String t = "admin"; // This works — but only because both are now pool objects System.out.println(s == t); // true // Problem: the next developer who reads this code // does not know whether intern() was called upstream // Using equals() makes the intent clear regardless of pool state System.out.println(t.equals(s)); // true — always works, always readable

Interview Questions

Q1. What is the Java String Pool and where is it located?

The String Pool (also called the String Constant Pool or Intern Pool) is a special region in heap memory where Java stores string literals. When the JVM encounters a string literal, it checks the pool first — if the string already exists, it returns the existing reference instead of creating a new object. This optimisation avoids duplicate string objects in memory. In Java 8 and later, the pool lives inside the main heap. In Java 7 and earlier, it was in PermGen — a separate, fixed-size region that was removed in Java 8 due to its size limitations.

Q2. What is the difference between creating a String with a literal and with new String()?

A string literal — String s = "hello" — checks the String Pool first. If "hello" exists, the existing reference is returned. No new object is created. Using new String("hello") always creates a new object on the regular heap, bypassing the pool entirely. Two variables assigned the same literal may share the same object in memory. Two variables assigned via new String() with the same content always refer to different objects. == returns true for same-literal references and false for new String() references, even with identical content.

Q3. What does the intern() method do?

intern() returns the canonical pool entry for a String. If the pool already contains a string equal to this one, it returns that reference. If not, it adds this string to the pool and returns the reference. The main use case is memory optimisation: when a system holds millions of identical strings as heap objects, calling intern() on them deduplicates them into single pool entries. In modern Java, intern() is rarely needed for application code — it has overhead, and equals() handles comparison correctly without it.

Q4. Why does == sometimes return true for Strings even without calling equals()?

Because of the String Pool. When two string literals have the same content, the JVM returns the same pool reference for both. Since == compares references, and both variables point to the same pool object, == returns true. This creates an illusion that == works for string content comparison. It breaks immediately when one string comes from outside the pool — a database query, an API response, user input, or a new String() call — all of which produce new heap objects. The only safe content comparison is equals().

Q5. Why is String immutability necessary for the String Pool to work?

If strings were mutable, a change through one reference would silently affect all other variables pointing to the same pool entry. For example, if city1 = "Delhi" and city2 = "Delhi" both point to the same pool entry, and city1 could change the characters to "Mumbai", then city2 would silently read "Mumbai" — a catastrophic side effect. Immutability guarantees that the pool entry never changes once created, making it safe for any number of references to share the same object without risk of interference.

Q6. When should you use intern() in production code?

Rarely. intern() is appropriate when a system processes enormous volumes of data where the same strings repeat millions of times — log parsers, data pipelines, read-heavy caches. Calling intern() in these scenarios reduces heap usage by replacing many duplicate heap objects with single pool entries. It should not be used as a general-purpose practice: it adds synchronisation overhead (the pool is shared), complicates code, and provides no benefit for typical application strings like usernames, product names, or addresses that appear only a few times each.

FAQs

Are String Pool entries ever garbage collected?

In Java 7 and later, the pool lives in the main heap and pool entries are eligible for garbage collection when no references point to them — just like regular objects. This was a significant improvement over Java 6, where pool entries in PermGen were effectively never collected. In practice, literals declared in loaded classes are kept alive as long as the class is loaded, so very few pool entries are actually collected during normal application runtime.

Does the String Pool affect performance?

Pool lookup adds a small overhead compared to simple heap allocation — the JVM must check whether the string exists before deciding to create or reuse. However, the memory savings from pool sharing — especially in applications with many repeated literals like configuration values and status codes — far outweigh the tiny lookup cost. The overall effect is typically a performance improvement due to reduced garbage collection pressure.

Can you put any String into the pool?

Yes, using intern(). Any String object — regardless of how it was created — can be put into the pool by calling intern(). If the pool already has an equal string, the existing pool reference is returned. If not, the string is added to the pool. Literals are automatically interned at class load time by the JVM without any explicit intern() call.

What happens to the new String("hello") argument — is "hello" in the pool?

Yes. Writing new String("hello") actually involves two objects: the literal "hello" inside the constructor call, which is placed in the pool, and the new String object on the heap. So after new String("hello"), there is one pool entry for "hello" and one separate heap object that is also "hello". The heap object is what the variable references. The pool entry exists but is only accessible through the pool lookup or via the literal "hello" elsewhere in the code.

Is the String Pool thread-safe?

Yes. The JVM synchronises access to the String Pool internally — multiple threads can request pool entries simultaneously without corruption or race conditions. This is one reason intern() has overhead: every call requires acquiring a lock on the pool. For this reason, avoid calling intern() in high-throughput hot paths where the lock contention could become measurable.

Summary

The String Pool is Java's built-in mechanism for reusing identical string objects rather than creating duplicates. String literals automatically go into the pool. new String() bypasses it. intern() bridges the gap, moving a heap String into the pool. Since Java 8, the pool lives in the main heap and participates in normal garbage collection.

The pool is why == sometimes appears to work for string comparison — when both strings are literals pointing to the same pool entry. It is also precisely why relying on == is dangerous: the moment a string arrives from outside the pool, the comparison breaks silently. The lesson is always the same: use equals() for content comparison, use == only for checking null or intentional reference identity.

For interviews, be ready to draw the memory diagram showing literals in the pool and new String() on the heap, explain why immutability is a prerequisite for pool sharing, describe what intern() does and when it is genuinely useful, and explain why strings from databases and APIs are always heap objects.

What to Read Next

Practice Programs

Put this topic into practice — each program has full code, output and a step-by-step explanation you can run online.

Browse all Java programs →