Java String Pool vs Heap: Where Strings Live
java string pool vs heap: Understand how Java stores strings in the pool versus the heap, how interning works, and when each approach fits your memory needs.
When you create a string in Java, where it lives depends on how you created it. The java string pool vs heap distinction determines whether two strings with the same characters share memory or occupy separate objects. This matters for memory usage, reference equality checks, and how long string data survives in the JVM.
How String Literals Reach the Pool
When the JVM loads a class, it processes the constant pool embedded in the class file. String literals that appear in the source code are stored as entries in this constant pool. At runtime, the JVM places these literals into the string pool, a dedicated area of memory managed by the JVM.
Consider this example:
String greeting = "hello"; String other = "hello";
Both variables reference the same object from the string pool. The JVM guarantees that identical string literals resolve to the same pooled instance. That is why greeting == other evaluates to true here, even though you never called an interning method explicitly.
The pool is not part of the regular object heap in the same way as ordinary objects. Historically it lived in the permanent generation, and since Java 7 it has been part of the main heap. That change matters for garbage collection: pooled strings are now collected like other heap objects when they become unreachable.
What Happens When You Use new String()
The new keyword changes the behavior completely:
String first = new String("hello"); String second = new String("hello");
Each call to new String("hello") creates a separate object on the heap. The literal "hello" inside the constructor still resolves to the pooled instance, but the constructor copies its contents into a new object. first == second is false because they are distinct heap objects, even though first.equals(second) is true.
This is the core difference in the java string pool vs heap comparison: pooled strings are shared by identity, while heap-allocated strings are distinct objects that happen to contain the same character sequence.
How Interning Works
The intern() method gives you explicit control over whether a string joins the pool:
String dynamic = new String("hello"); String pooled = dynamic.intern();
If the pool already contains a string equal to dynamic, intern() returns that pooled instance. If not, it adds dynamic to the pool and returns it. After this call, pooled == dynamic.intern() is true, and pooled == "hello" is also true.
Interning is useful when you have many strings that repeat across a large dataset, such as identifiers read from a file or keys built from multiple parts. By interning them, you collapse duplicates into a single shared object. The tradeoff is that the pool retains references to interned strings, so they are not eligible for garbage collection while the pool itself is alive. A large number of unique interned strings can therefore increase memory pressure permanently.
When the Pool Grows and Shrinks
Since Java 7, the string pool is part of the heap, which means pooled strings can be collected when no live reference points to them. Before Java 7, the pool lived in the permanent generation and was not collected in the same way, which often caused OutOfMemoryError: PermGen space in applications that interned many distinct strings.
In modern JVMs, you can observe pool behavior through the -XX:StringTableSize tuning flag, which controls the number of buckets in the hash table backing the pool. A small table increases collision rates and slows down interning lookups. A very large table wastes memory. The default size depends on the JVM version and platform, so check the documentation for your specific runtime before changing it.
Performance and Memory Tradeoffs
The java string pool vs heap decision is mostly a memory tradeoff. Pooled strings save memory when the same character sequence appears many times. They cost memory when the pool contains many unique strings that are never reused.
Reference equality checks on pooled strings are faster than equals() calls because they compare object identity instead of walking the character array. This is why some codebases intern strings used as map keys or enum-like constants. However, relying on == for string comparison is fragile: it only works if every string in the comparison path has been interned. A single new String() in the chain breaks the assumption.
For most application code, equals() is the correct comparison method. Interning should be reserved for cases where you have measured or clearly identified a high duplication rate, or where identity comparison is part of the design, such as a fixed set of canonical constants.
Choosing Between Pooled and Heap Strings
Use string literals for constants and configuration values known at compile time. They are automatically pooled and cost nothing extra.
Use new String() only when you genuinely need a fresh copy, for example when you want to break a reference to a large backing array. The String(String) constructor exists for this purpose, but it is rarely necessary in modern code.
Use intern() when you process a bounded set of repeated values at runtime and want to collapse duplicates. Be cautious with unbounded input: interning every distinct value from user input can fill the pool and cause memory problems.
A practical pattern for canonical values is to keep your own lookup structure instead of relying on the global pool:
private final Map<String, String> canonicalNames = new ConcurrentHashMap<>(); public String canonicalize(String name) { return canonicalNames.computeIfAbsent(name, Function.identity()); }
This gives you the deduplication benefit of interning without affecting the global string pool. The map controls the lifetime of the canonical instances, so you can clear it or let it be collected when the owning object goes away.
How the Pool Behaves Under Garbage Collection
Because the pool is part of the heap in modern JVMs, interned strings are subject to normal garbage collection when no live references exist. This is a meaningful difference from the permanent generation era. However, the pool itself holds references to the strings it contains, so an interned string stays alive as long as the pool references it. The pool is a root for these objects in practice.
This means the memory cost of interning is not automatically reclaimed when the original variable goes out of scope. The pooled instance remains until the pool entry is removed, which happens only when the string becomes unreachable from the pool's perspective. In practice, the pool is a hash table that keeps entries alive, so unique interned strings tend to persist for the lifetime of the JVM unless the table resizes or entries are explicitly removed, which the standard API does not expose.
If you need deduplication with reclaimable memory, a custom canonical map as shown above is usually the better choice. It ties the lifetime of canonical strings to the lifetime of the map, which you control.