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.

1// File: LiteralVsNewDemo.java 2 3public class LiteralVsNewDemo { 4 5 public static void main(String[] args) { 6 7 // Way 1 — String literal 8 // JVM checks the pool first. 9 // "Java" does not exist yet → creates it in the pool 10 String s1 = "Java"; 11 12 // "Java" already exists in the pool → reuses the same object 13 String s2 = "Java"; 14 15 // Way 2 — new String() 16 // Always creates a new object in the regular heap 17 // Bypasses the pool entirely 18 String s3 = new String("Java"); 19 String s4 = new String("Java"); 20 21 System.out.println("=== Reference Comparison (==) ==="); 22 System.out.println("s1 == s2 : " + (s1 == s2)); // true — same pool object 23 System.out.println("s1 == s3 : " + (s1 == s3)); // false — pool vs heap 24 System.out.println("s3 == s4 : " + (s3 == s4)); // false — two separate heap objects 25 System.out.println("s2 == s4 : " + (s2 == s4)); // false — pool vs heap 26 27 System.out.println(); 28 29 System.out.println("=== Content Comparison (equals) ==="); 30 System.out.println("s1.equals(s2) : " + s1.equals(s2)); // true 31 System.out.println("s1.equals(s3) : " + s1.equals(s3)); // true 32 System.out.println("s3.equals(s4) : " + s3.equals(s4)); // true 33 34 System.out.println(); 35 36 // Identity hash code confirms which objects are the same 37 System.out.println("=== Identity Hash Codes ==="); 38 System.out.println("s1 id: " + System.identityHashCode(s1)); 39 System.out.println("s2 id: " + System.identityHashCode(s2)); // same as s1 40 System.out.println("s3 id: " + System.identityHashCode(s3)); // different 41 System.out.println("s4 id: " + System.identityHashCode(s4)); // different from s3 42 } 43}
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)
1// File: PoolStepByStepDemo.java 2 3public class PoolStepByStepDemo { 4 5 public static void main(String[] args) { 6 7 String city1 = "Mumbai"; // created in pool 8 String city2 = "Mumbai"; // reuses pool entry 9 String city3 = new String("Mumbai"); // new heap object 10 String city4 = city3.intern(); // returns pool entry 11 12 System.out.println("city1 == city2 : " + (city1 == city2)); // true — same pool obj 13 System.out.println("city1 == city3 : " + (city1 == city3)); // false — pool vs heap 14 System.out.println("city1 == city4 : " + (city1 == city4)); // true — intern returns pool obj 15 System.out.println("city3 == city4 : " + (city3 == city4)); // false — heap vs pool 16 17 System.out.println(); 18 19 System.out.println("All have same content:"); 20 System.out.println("city1.equals(city3) : " + city1.equals(city3)); // true 21 System.out.println("city3.equals(city4) : " + city3.equals(city4)); // true 22 } 23}
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.

1// File: InternDemo.java 2 3public class InternDemo { 4 5 public static void main(String[] args) { 6 7 // Scenario 1 — intern() returns the pool reference 8 String heapStr = new String("developer"); 9 String poolStr = heapStr.intern(); // returns pool reference 10 String literal = "developer"; // already in pool 11 12 System.out.println("=== intern() basics ==="); 13 System.out.println("heapStr == poolStr : " + (heapStr == poolStr)); // false — heap vs pool 14 System.out.println("poolStr == literal : " + (poolStr == literal)); // true — same pool obj 15 System.out.println("heapStr == literal : " + (heapStr == literal)); // false 16 17 System.out.println(); 18 19 // Scenario 2 — intern() of a new string not yet in pool 20 String built1 = new String("freshtech"); 21 String built2 = new String("freshtech"); 22 23 System.out.println("=== intern() of new strings ==="); 24 System.out.println("built1 == built2 : " + (built1 == built2)); // false 25 System.out.println("built1.intern() == built2.intern(): " 26 + (built1.intern() == built2.intern())); // true — both intern to same pool entry 27 28 System.out.println(); 29 30 // Scenario 3 — performance use case 31 // In systems that process millions of repeated strings (like log levels, status codes) 32 // internment avoids holding duplicate objects in memory 33 34 String[] statuses = { 35 new String("SUCCESS"), 36 new String("FAILED"), 37 new String("SUCCESS"), 38 new String("PENDING"), 39 new String("SUCCESS"), 40 new String("FAILED"), 41 }; 42 43 System.out.println("=== Before intern — each is a separate object ==="); 44 System.out.println("statuses[0] == statuses[2] : " + (statuses[0] == statuses[2])); // false 45 46 // Intern all — now identical strings share the same object 47 for (int i = 0; i < statuses.length; i++) { 48 statuses[i] = statuses[i].intern(); 49 } 50 51 System.out.println("After intern — identical strings share one object:"); 52 System.out.println("statuses[0] == statuses[2] : " + (statuses[0] == statuses[2])); // true 53 System.out.println("statuses[1] == statuses[5] : " + (statuses[1] == statuses[5])); // true 54 System.out.println("statuses[0] == statuses[1] : " + (statuses[0] == statuses[1])); // false 55 } 56}
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.

1// File: CompileTimeDemo.java 2 3public class CompileTimeDemo { 4 5 public static void main(String[] args) { 6 7 // Compile-time constants — compiler folds at compile time 8 String s1 = "hello"; 9 String s2 = "hel" + "lo"; // compile-time concat → same as "hello" 10 String s3 = "hel" + "lo" + " world"; // compile-time concat 11 12 final String prefix = "hel"; // final with literal — compile-time constant 13 String s4 = prefix + "lo"; // compile-time concat — final + literal 14 15 System.out.println("=== Compile-time constants ==="); 16 System.out.println("s1 == s2 : " + (s1 == s2)); // true — both "hello" in pool 17 System.out.println("s1 == s4 : " + (s1 == s4)); // true — final + literal = compile-time 18 19 System.out.println(); 20 21 // Runtime concatenation — creates a new heap object 22 String base = "hel"; // NOT final — runtime variable 23 String s5 = base + "lo"; // runtime concat — new heap object 24 25 String part1 = "hel"; // final but NOT declared final 26 // Actually let's use a method call which is always runtime 27 String s6 = "hel".concat("lo"); // method call — always runtime 28 29 System.out.println("=== Runtime concatenation ==="); 30 System.out.println("s1 == s5 : " + (s1 == s5)); // false — s5 on heap 31 System.out.println("s1 == s6 : " + (s1 == s6)); // false — s6 on heap 32 33 System.out.println(); 34 35 System.out.println("Content is still equal:"); 36 System.out.println("s1.equals(s5) : " + s1.equals(s5)); // true 37 System.out.println("s1.equals(s6) : " + s1.equals(s6)); // true 38 39 System.out.println(); 40 41 // Final variable with literal value = compile-time constant 42 final String A = "hello"; 43 final String B = "world"; 44 String AB = A + " " + B; // compile-time concat (all finals + literal) 45 String ab = "hello" + " " + "world"; // also compile-time 46 47 System.out.println("=== final variables ==="); 48 System.out.println("AB == ab : " + (AB == ab)); // true — both in pool 49 System.out.println("AB == 'hello world': " + (AB == "hello world")); // true 50 } 51}
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.

1// File: ImmutabilityPoolDemo.java 2 3public class ImmutabilityPoolDemo { 4 5 public static void main(String[] args) { 6 7 String city1 = "Delhi"; 8 String city2 = "Delhi"; // same pool object as city1 9 10 System.out.println("city1 == city2 : " + (city1 == city2)); // true — same object 11 System.out.println("city1 : " + city1); 12 System.out.println("city2 : " + city2); 13 14 System.out.println(); 15 16 // Strings are immutable — you cannot change the characters 17 // city1.charAt(0) = 'B'; // would not compile — no such method 18 // The only thing you can do is make city1 point to a NEW string 19 20 city1 = city1.replace("Delhi", "Bengaluru"); 21 // replace() returned a NEW String — city1 now points to "Bengaluru" 22 // city2 still points to "Delhi" in the pool — UNCHANGED 23 24 System.out.println("After city1 = city1.replace('Delhi', 'Bengaluru'):"); 25 System.out.println("city1 : " + city1); // Bengaluru 26 System.out.println("city2 : " + city2); // Delhi — untouched 27 28 System.out.println(); 29 30 // Why this matters — thread safety 31 // Multiple threads can hold references to the same pooled String 32 // None of them can modify the shared string → no synchronisation needed 33 System.out.println("=== Thread Safety Benefit ==="); 34 System.out.println("Both threads can read 'Delhi' safely:"); 35 System.out.println("Thread 1 sees: " + city2); 36 System.out.println("Thread 2 sees: " + city2); // always "Delhi" — cannot change 37 38 System.out.println(); 39 40 // Why == sometimes seems to work for strings — the pool creates this illusion 41 System.out.println("=== The == Illusion From the Pool ==="); 42 String role1 = "admin"; // pool 43 String role2 = "admin"; // pool — reuses same object 44 45 System.out.println("role1 == role2 : " + (role1 == role2)); // true — LUCKY (pool) 46 47 // But this breaks when strings come from outside the pool 48 String userInput = getUserRole(); // simulates real-world — returns heap String 49 System.out.println("userInput == 'admin' : " + (userInput == "admin")); // false — BREAKS 50 System.out.println("'admin'.equals(userInput): " + "admin".equals(userInput)); // true — CORRECT 51 } 52 53 static String getUserRole() { 54 // In real systems, strings come from databases, APIs, user forms 55 // These are always new heap objects — never pool objects 56 return new String("admin"); 57 } 58}
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

Real-World Example — Configuration and Status Code Management

The Business Problem

A backend service at a company like Swiggy or PhonePe handles millions of order events per second. Each event carries status strings like "PLACED", "CONFIRMED", "DELIVERED", "FAILED". Without the String pool, millions of identical status strings would occupy separate heap objects — wasting memory. Understanding the pool also explains why configuration values, enum-like constants, and fixed status codes should always be string literals or final constants — they benefit from pool sharing automatically.

1// File: OrderStatus.java 2 3public final class OrderStatus { 4 5 // These are string literals — stored in the pool once 6 // Every reference to these constants points to the same pool objects 7 public static final String PLACED = "PLACED"; 8 public static final String CONFIRMED = "CONFIRMED"; 9 public static final String PACKED = "PACKED"; 10 public static final String SHIPPED = "SHIPPED"; 11 public static final String DELIVERED = "DELIVERED"; 12 public static final String CANCELLED = "CANCELLED"; 13 public static final String FAILED = "FAILED"; 14 15 private OrderStatus() {} 16 17 public static boolean isTerminal(String status) { 18 // Using equals() — correct even though these happen to be pool objects 19 // Never rely on == for status comparison in real code 20 return DELIVERED.equals(status) 21 || CANCELLED.equals(status) 22 || FAILED.equals(status); 23 } 24 25 public static boolean isActive(String status) { 26 return PLACED.equals(status) 27 || CONFIRMED.equals(status) 28 || PACKED.equals(status) 29 || SHIPPED.equals(status); 30 } 31}
1// File: OrderEvent.java 2 3public class OrderEvent { 4 5 private final String orderId; 6 private final String status; // comes from external system — heap String 7 private final long timestamp; 8 9 public OrderEvent(String orderId, String status, long timestamp) { 10 this.orderId = orderId; 11 // intern() normalises the status to the pool for memory efficiency 12 // Millions of "DELIVERED" events all share one pool entry 13 this.status = (status != null) ? status.intern() : null; 14 this.timestamp = timestamp; 15 } 16 17 public String getOrderId() { return orderId; } 18 public String getStatus() { return status; } 19 public long getTimestamp(){ return timestamp; } 20 21 @Override 22 public String toString() { 23 return orderId + " [" + status + "] at " + timestamp; 24 } 25}
1// File: EventProcessorDemo.java 2 3public class EventProcessorDemo { 4 5 public static void main(String[] args) { 6 7 System.out.println("╔══════════════════════════════════════════╗"); 8 System.out.println("║ ORDER EVENT PROCESSING DEMO ║"); 9 System.out.println("╚══════════════════════════════════════════╝\n"); 10 11 // Simulate events arriving from external systems 12 // In reality these come from Kafka, HTTP, or a DB query — all heap Strings 13 OrderEvent[] events = { 14 new OrderEvent("ORD-001", new String("PLACED"), 1000L), 15 new OrderEvent("ORD-002", new String("CONFIRMED"), 1001L), 16 new OrderEvent("ORD-001", new String("CONFIRMED"), 1002L), 17 new OrderEvent("ORD-003", new String("DELIVERED"), 1003L), 18 new OrderEvent("ORD-002", new String("DELIVERED"), 1004L), 19 new OrderEvent("ORD-004", new String("FAILED"), 1005L), 20 new OrderEvent("ORD-001", new String("DELIVERED"), 1006L), 21 }; 22 23 System.out.println("=== Processing Events ===\n"); 24 int delivered = 0, failed = 0, active = 0; 25 26 for (OrderEvent event : events) { 27 String status = event.getStatus(); 28 29 // After intern() in constructor, == against pool constants works 30 // But we ALWAYS use equals() in real production code 31 String category; 32 if (OrderStatus.isTerminal(status)) { 33 category = "TERMINAL"; 34 if (OrderStatus.DELIVERED.equals(status)) delivered++; 35 else failed++; 36 } else { 37 category = "ACTIVE"; 38 active++; 39 } 40 41 System.out.printf(" %-12s %-12s [%s]%n", 42 event.getOrderId(), status, category); 43 } 44 45 System.out.println(); 46 System.out.println("=== Summary ==="); 47 System.out.println("Active : " + active); 48 System.out.println("Delivered : " + delivered); 49 System.out.println("Failed : " + failed); 50 51 System.out.println(); 52 53 // Verify pool sharing after intern() 54 System.out.println("=== Pool Sharing After intern() ==="); 55 String s1 = events[2].getStatus(); // "CONFIRMED" — interned 56 String s2 = events[0].getStatus(); // "PLACED" — interned 57 String constConfirm = OrderStatus.CONFIRMED; // pool literal 58 59 System.out.println("event[2].status == OrderStatus.CONFIRMED: " 60 + (s1 == constConfirm)); // true — both in pool after intern() 61 62 System.out.println("(still use equals() in production — not ==)"); 63 64 System.out.println(); 65 66 // Configuration values — always use literals for pool benefit 67 System.out.println("=== Configuration Constants ==="); 68 String dbHost1 = "db.internal.swiggy.com"; // pool 69 String dbHost2 = "db.internal.swiggy.com"; // same pool object 70 71 System.out.println("dbHost1 == dbHost2 : " + (dbHost1 == dbHost2)); // true — pool 72 System.out.println("dbHost1.equals(dbHost2): " + dbHost1.equals(dbHost2)); // true 73 System.out.println("Memory saved by pool : " + (dbHost1 == dbHost2 ? "YES" : "NO")); 74 } 75}
Output:
╔══════════════════════════════════════════╗
║     ORDER EVENT PROCESSING DEMO         ║
╚══════════════════════════════════════════╝

=== Processing Events ===

  ORD-001      PLACED       [ACTIVE]
  ORD-002      CONFIRMED    [ACTIVE]
  ORD-001      CONFIRMED    [ACTIVE]
  ORD-003      DELIVERED    [TERMINAL]
  ORD-002      DELIVERED    [TERMINAL]
  ORD-004      FAILED       [TERMINAL]
  ORD-001      DELIVERED    [TERMINAL]

=== Summary ===
Active    : 3
Delivered : 3
Failed    : 1

=== Pool Sharing After intern() ===
event[2].status == OrderStatus.CONFIRMED: true
(still use equals() in production — not ==)

=== Configuration Constants ===
dbHost1 == dbHost2     : true
dbHost1.equals(dbHost2): true
Memory saved by pool   : YES

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

1// Unnecessary heap object — always wastes memory 2String status = new String("PLACED"); 3 4// Correct — literal goes into pool, reused on next identical declaration 5String 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

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

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

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

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

1// Interning and using == is NOT a replacement for equals() 2String s = new String("admin").intern(); 3String t = "admin"; 4 5// This works — but only because both are now pool objects 6System.out.println(s == t); // true 7 8// Problem: the next developer who reads this code 9// does not know whether intern() was called upstream 10// Using equals() makes the intent clear regardless of pool state 11System.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