Q24 of 40 · Core Java
What is the difference between String pool and heap memory for String objects?
Short answer
Short answer: String literals and strings produced by intern() are stored in the String pool (inside the heap in Java 7+), where identical strings share a single object. Strings created with `new String()` are always separate heap objects even if their content matches a pooled string. Pool lookups are O(1) hash lookups.
Detail
String pool (string intern pool): a hash table maintained by the JVM inside the heap (moved from PermGen to main heap in Java 7). When the compiler encounters a string literal like "hello", it checks the pool. If "hello" is already there, it reuses the reference; if not, it adds a new entry. This means two variables assigned "hello" literally point to the exact same object, and == returns true (the Integer cache analogy).
Heap strings: new String("hello") always allocates a fresh object in the regular heap, even if "hello" is already in the pool. The new object has its own identity — == on two new String("hello") instances returns false. This was the old way to bypass pooling (for security: password strings you wanted to scrub by zeroing a char[]), but since String became immutable and the pool moved to the main heap, this pattern is mostly obsolete.
String.intern(): explicitly checks the pool for a matching string and returns the canonical pooled version. Useful when you're generating millions of short strings (e.g., parsing log files) and want deduplication — but it can cause pool contention under heavy multi-threaded use.
For test engineers: the most important practical implication is the == trap when comparing strings returned by API responses, parsed JSON, or built at runtime — none of these are pooled. Always use .equals(). The pool also explains why assertThat(response.field()).isEqualTo("expected") works correctly (AssertJ calls .equals()), but a hand-rolled if (response.field() == "expected") is a latent bug.
// EXAMPLE
StringPool.java
// String literals — pooled, share reference
String a = "hello";
String b = "hello";
System.out.println(a == b); // true — same pooled object
System.out.println(a.equals(b)); // true — same content
// new String() — always a new heap object
String c = new String("hello");
String d = new String("hello");
System.out.println(c == d); // false — different heap objects
System.out.println(c.equals(d)); // true — same content
// String.intern() — look up or add to pool
String e = new String("hello").intern();
System.out.println(a == e); // true — intern() returns pooled ref
// Concatenation at runtime — NOT pooled
String prefix = "hel";
String suffix = "lo";
String combined = prefix + suffix; // new heap object
System.out.println(a == combined); // false — NOT the pooled "hello"
System.out.println(a.equals(combined)); // true — same content
// Compile-time constants ARE pooled (compiler folds them)
String constant = "hel" + "lo"; // compiler folds to "hello" literal
System.out.println(a == constant); // true — folded at compile time