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
| 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 |
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 unaffectedMistake 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 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.