Java String Pool
How the Java String pool works, why string literals are interned, and the intern() method.
A typical Java program creates thousands of strings, and a huge fraction of them are the same characters as some other string elsewhere in the program. Method names. Configuration keys. Error messages. Field labels. The JVM treats this redundancy as worth solving — it keeps a special region called the string pool (or string intern table) and gives every literal that appears in source code a shared entry there. Two literals with the same characters end up pointing at the same object.
That sharing has visible consequences for identity comparison (==), memory use, and a small number of subtle bugs around intern(). This chapter is about the rules.
Literals are pooled, new String(...) is not
The single most important fact:
String a = "hello";
String b = "hello";
System.out.println(a == b); // true — same pooled object
String c = new String("hello");
System.out.println(a == c); // false — new String, fresh objectEvery string literal that the compiler sees is added to the pool the first time it's loaded. Subsequent occurrences of the same literal — anywhere in the program, in any class — get back the same reference. So a and b are the same object.
new String("hello") forces a fresh allocation on the heap. The argument "hello" is still pooled (because it's a literal), but the constructor copies it into a brand-new object outside the pool. c and a therefore have equal contents but different identities.
This is the whole reason "use equals, not ==" is drilled into every Java textbook. Identity comparison happens to work for plain literals but breaks the moment a string comes from new, from a parser, from network input, or from concatenation that the compiler didn't fold at compile time.
What lives in the pool
The pool is populated by two routes:
- String literals in source code. The compiler emits each unique literal as a
CONSTANT_Stringentry in the class's constant pool; the JVM resolves it into a realStringobject in the heap-resident pool the first time the class uses it. - Explicit
intern()calls. AnyStringyou have a reference to can be added to the pool by callings.intern(). The method returns the pooled instance — which is the same reference for every caller that interns equal contents.
Computed strings — a + b, s.substring(...), results of String.format — are not pooled automatically. They live wherever the GC put them and have whatever identity they happen to have.
String x = "java";
String y = "ja" + "va"; // compile-time constant — pooled, == x
String z = "ja" + new String("va"); // runtime computation — NOT pooled
System.out.println(x == y); // true
System.out.println(x == z); // false
System.out.println(x == z.intern()); // true — intern() returns the pooled instanceThat second case is the trap. y is computed from two literals, but the compiler folds the concatenation at compile time, so the result is just another literal — pooled. z involves a runtime new, the compiler can't fold it, and the resulting object lives off-pool.
The intern() method
String#intern() does two things in one call:
- If a string with the same characters is already in the pool, return that pooled reference.
- Otherwise, add this string to the pool and return it.
That second behaviour is the useful one when you build strings at runtime from a small but high-frequency vocabulary — HTTP header names being parsed from bytes, tokens from a lexer, column names being read out of a database driver. Interning them collapses N separate objects into one and means downstream comparisons can use == if you've measured it as worth the trouble.
String s1 = new String("status").intern();
String s2 = new String("status").intern();
System.out.println(s1 == s2); // true — both refer to the pooled "status"The catch: every intern() call costs a hash lookup, and pooled strings live in a fixed-size hash table that doesn't shrink. If you intern unbounded input (user-typed search queries, request IDs), you slowly fill the pool with strings that will never be reused — a memory leak in slow motion. Intern only when (a) the set of values is bounded and (b) you've measured a problem worth solving.
Pool internals (lightly)
The pool is implemented as a hash table inside the JVM. On HotSpot it's a StringTable with a default capacity that's been tuned upward over the years (currently 65,536 buckets on most builds). You can inspect it at the command line:
java -XX:+PrintStringTableStatistics MyAppFor application code, the implementation is invisible: you can't ask "is this string in the pool?" via the public API, and you don't need to. The visible behaviour is == on equal literals, and intern() for opting computed strings in.
Why == is still wrong for strings
The pool can make == appear to work on test inputs:
String a = "hello";
String b = "hello";
if (a == b) { ... } // happens to be trueThen someone passes the string through BufferedReader.readLine() and == silently turns false. The contract you want is "do these have the same characters?", and that contract is spelled a.equals(b). The pool is a memory optimisation, not a comparison strategy — never rely on it for correctness.
A worked example
The example below makes the pool's behaviour visible. Each printRef call shows the system identity hash (a one-line stand-in for "which object is this?") so you can see where literals share storage and where computed strings don't.
Read the identity hashes first: the literals and the compile-time fold share one. runtimeConcat and fresh each have their own. interned matches the literal again, because intern() returned the pooled instance, not the new-allocated one. The == results follow directly from the identities; equals returns true for all of them because, contents-wise, they really are equal.
What's next
The pool exists because String is immutable — sharing the same object between callers is only safe if no one can change its contents. The next chapter pulls on that thread: why immutability was chosen, what it buys you, and the design trade-off it forces. Continue to Java String immutability.