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
| Aspect | String s = "hello" | String s = new String("hello") | s.intern() |
|---|---|---|---|
| Memory location | String Pool | Regular Heap | String Pool (adds if absent, returns existing) |
| Checks pool first | Yes | No — always creates new | Yes |
| Creates new object | Only if not in pool | Always | Never — returns existing or adds |
== with same literal | true | false | true |
equals() with same content | true | true | true |
| Garbage collectible | No — pool entries are GC roots | Yes — if no references | Depends on Java version |
| When to use | Almost always | Rarely — when explicit heap object needed | Memory-intensive systems with repeated strings |
| Performance | Fast — pool lookup | Slightly slower — heap allocation | Adds overhead — use only when justified |
| Java version behaviour | Same across all versions | Same | Java 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:
- ›
aandbare both string literals with the same text, so the JVM reuses the same pool entry for both —a == bistrue. - ›
cis created withnew String(...), which always creates a separate object on the regular heap, bypassing the pool — soa == cisfalseeven though the content matches. - ›
c.intern()looks up"cat"in the pool and returns the existing pooled reference — the same oneaandbalready point to. - ›
a == distruebecausednow 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 unaffectedMistake 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 readableInterview 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 →