Introduction
The string constant pool java was introduced in Java 1.0 (May 1996) as a way to deduplicate string literals in bytecode and is one of the oldest performance features of the JVM. Each String literal in your source code is stored once in the constant pool of the class file and is interned into a runtime string pool when the class is loaded. The String.intern() method, also present since Java 1.0, lets you add strings created at runtime to the same pool so that subsequent equal strings share the same reference. In Java 7 (July 2011), the pool was moved from the PermGen to the ordinary Java heap, which fundamentally changed how it interacts with garbage collection.
For a Java learner, the string constant pool java is the difference between a == b being true or false for two strings that contain the same characters, and that difference is the cause of countless subtle bugs. Knowing when new String("x") produces a fresh heap object, when "a" + "b" is folded by the compiler into a single literal, and when intern() saves you memory at the cost of a hash-table lookup is essential for writing efficient string-heavy code. This post walks through every aspect of the pool with a runnable demo and explains why Java 9 (September 2017, JEP 254 Compact Strings) replaced the underlying char[] with a byte[] for ASCII strings.
What is the String Constant Pool in Java?
The string constant pool in Java is a JVM-managed hash table that stores references to unique String objects so that two equal string literals share the same String instance. The pool has two halves: the static pool (the constant pool of each class file, populated at compile time with literals like "hello" and constant-folded expressions like "a" + "b") and the dynamic pool (the runtime hash table that lives on the heap, populated by class loading and by explicit calls to String.intern()). The dynamic pool is what makes "hello" == "hello" evaluate to true at runtime.
The category breakdown is: pooled strings (literals and intern()ed strings, which compare equal by reference) versus heap strings (strings created by new String(), by String.valueOf(new char[]{...}), by StringBuilder.toString(), or by runtime concatenation with a non-constant operand, which are distinct objects even when they contain the same characters). The pool itself lives on the heap (Java 7+), is garbage-collected when no live references remain, and was previously in PermGen (Java 6 and earlier) which could cause OutOfMemoryError: PermGen. The pool's behaviour is governed by the JVMS (Java Virtual Machine Specification) section 5.3 and by the String class's contract for intern().
How the String Constant Pool Works - Step by Step
Step 1: Compile-time literals. When you write String a = "java"; in source, the compiler emits a constant pool entry of type CONSTANT_String_info in the class file, and the JVMS resolves that entry to a String instance when the class is loaded. The resolution process checks the string pool: if an equal String is already there, the existing reference is used; if not, a new String is created and added to the pool. This means String a = "java"; String b = "java"; produces a single String instance and a == b is true.
Step 2: Compile-time concatenation. The compiler folds constant expressions like "ja" + "va" into a single literal "java" at compile time, so "ja" + "va" == "java" is true. This is specified by JLS section 15.29 (Constant Expressions). The folding also applies to final variables that are initialised with constant expressions, so final String prefix = "ja"; String c = prefix + "va"; also produces the pooled "java". But if the variable is not final, or is not initialised with a constant expression, the concatenation happens at runtime via StringBuilder and produces a new heap String.
Step 3: Runtime concatenation. String a = "ja"; String b = a + "va"; compiles to new StringBuilder().append(a).append("va").toString(), and StringBuilder.toString() calls new String(char[]) which always creates a fresh heap object. So b == "java" is false. The compiler cannot fold this because a is a non-final variable and could in principle be reassigned between the assignment and the concatenation. To pool the result, you call b.intern() explicitly, which returns the pooled instance (creating it if necessary).
Step 4: new String(). new String("java") explicitly creates a new String object on the heap, even though the literal "java" is already in the pool. The constructor String(String) copies the char array (or byte array in Java 9+) into a fresh backing array, so new String("java") == "java" is false. This is almost always a mistake -- you should just use the literal directly -- but it is occasionally useful when you want to trim a large String's backing array (e.g., new String(hugeString.substring(0, 3)) in Java 6 where substring shared the backing array).
Step 5: intern(). String.intern() looks up the receiver in the pool; if found, returns the pooled instance, otherwise adds the receiver to the pool and returns it. The cost is a hash-table lookup plus, on miss, an insertion. In Java 6 the pool lived in PermGen and was never garbage collected, so careless intern() of generated strings could exhaust PermGen. In Java 7+ the pool lives on the heap and is garbage collected, so intern() is safe but still costs CPU on every lookup. G1 (Java 8u20, JEP G1StringDedup) can deduplicate the backing arrays of equal strings without your code calling intern(), which is the modern hands-off approach.
Java Program for the String Constant Pool
public class StringPoolDemo {
public static void main(String[] args) {
// --- Literal comparison ---
String a = "java";
String b = "java";
System.out.println("a == b (literals): " + (a == b));
// --- new String creates a fresh heap object ---
String c = new String("java");
System.out.println("a == c (new String): " + (a == c));
System.out.println("a == c.intern(): " + (a == c.intern()));
// --- Constant-folding at compile time ---
String d = "ja" + "va";
System.out.println("a == d (constant concat): " + (a == d));
// --- Runtime concat (StringBuilder) ---
String e = "ja";
String f = e + "va";
System.out.println("a == f (runtime concat): " + (a == f));
System.out.println("a == f.intern(): " + (a == f.intern()));
// --- Identity hashCodes ---
System.out.println("identityHashCode a: " + System.identityHashCode(a));
System.out.println("identityHashCode b: " + System.identityHashCode(b));
System.out.println("identityHashCode c: " + System.identityHashCode(c));
System.out.println("identityHashCode f: " + System.identityHashCode(f));
// --- intern() deduplicates ---
String[] arr = new String[5];
for (int i = 0; i < arr.length; i++) {
arr[i] = new String("dup").intern();
}
int distinct = 0;
for (int i = 0; i < arr.length; i++) {
boolean seen = false;
for (int j = 0; j < i; j++) {
if (arr[i] == arr[j]) { seen = true; break; }
}
if (!seen) distinct++;
}
System.out.println("Distinct interned refs: " + distinct);
// --- Compact strings (Java 9+): byte[] backing with coder ---
byte[] bytes = "abc".getBytes();
System.out.println("Bytes in 'abc': " + bytes.length);
}
}
Code Explanation - Line by Line
The first block compares two literal strings a and b that both contain "java". Because both literals are resolved through the string pool, they share the same String instance and a == b evaluates to true. This is the foundational property of the pool and the reason why == "works" for literals in many beginners' code -- but it is a fragile coincidence, not a reliable pattern.
The second block creates c via new String("java"). The literal "java" is resolved through the pool, but the constructor copies the char array into a fresh backing array and returns a new String object on the heap, so a == c is false. Calling c.intern() looks up the pooled instance and returns it, so a == c.intern() is true.
The third block tests compile-time concatenation. "ja" + "va" is a constant expression, so JLS 15.29 folds it to the literal "java" at compile time, and the result is pooled. So a == d is true.
The fourth block tests runtime concatenation. e + "va" involves a non-final variable e, so the compiler emits a StringBuilder.append chain and the result is a fresh heap String. So a == f is false, but a == f.intern() is true because intern() returns the pooled instance.
The fifth block prints identity hash codes, which are the underlying object addresses. a and b will share the same code (they are the same object), while c and f will have different codes (they are distinct objects).
The sixth block demonstrates intern() deduplication. Five separate new String("dup") calls would produce five distinct heap objects, but interning them all returns the same pooled instance. The nested loop counts distinct references by pointer equality, and the result is 1 -- all five entries are the same reference.
The seventh block prints the byte length of "abc".getBytes(). On Java 8 and earlier, String was backed by a char[] so "abc".getBytes() returned a 6-byte array (each char encoded as 2 bytes UTF-16). On Java 9+ with Compact Strings (JEP 254), String is backed by a byte[] with a coder flag, so ASCII strings use 1 byte per character and "abc".getBytes() returns a 3-byte array. This is the memory saving of Compact Strings.
Interview Questions and Answers
Q1. What is the String constant pool in Java?
The String constant pool is a JVM-managed hash table that stores references to unique String objects so that two equal string literals share the same instance. It has a static half (constant pool entries in each class file, populated at compile time) and a dynamic half (the runtime hash table on the heap, populated at class load time and by calls to String.intern()). The pool is what makes "a" == "a" evaluate to true.
Q2. What is the difference between == and equals() for Strings?
The == operator compares references -- it asks whether two String variables point at the same String object. The equals() method compares character sequences -- it asks whether two String objects contain the same characters. Because the pool deduplicates literals, "a" == "a" happens to be true, but new String("a") == "a" is false even though new String("a").equals("a") is true. You should always use equals() for string comparison, never ==.
Q3. What does String.intern() do?
String.intern() looks up the receiver in the string pool. If an equal String is already pooled, intern() returns that pooled instance; otherwise, intern() adds the receiver to the pool and returns it. The cost is a hash-table lookup (potentially on miss, an insertion), and in Java 6 the pool lived in PermGen and was never garbage collected, which made careless intern() of generated strings dangerous. In Java 7+ the pool lives on the heap and is garbage collected, so intern() is safe but still costs CPU.
Q4. Why does "a" + "b" == "ab" return true but a + "b" == "ab" return false (where a is a non-final String "a")?
When both operands of + are compile-time constant expressions, JLS section 15.29 requires the compiler to fold the expression into a single literal at compile time. So "a" + "b" is folded to the literal "ab" which is pooled, and "ab" == "ab" is true. But when one operand is a non-final variable, the compiler cannot fold (the variable could be reassigned between the assignment and the concatenation in principle), so it emits a StringBuilder.append chain and the result is a fresh heap String, so a + "b" == "ab" is false.
Q5. Where is the string pool located: PermGen or heap?
In Java 6 and earlier the string pool lived in the PermGen (permanent generation), a fixed-size area that was not garbage collected, so careless intern() of generated strings could throw OutOfMemoryError: PermGen. In Java 7 (July 2011) the pool was moved to the ordinary Java heap, which is garbage collected, so unused pooled strings can be reclaimed. In Java 8 PermGen was removed entirely and replaced by Metaspace (which only stores class metadata, not the string pool). Since Java 7, the answer is always "the heap".
Q6. What are Compact Strings (JEP 254) and how do they affect the pool?
Compact Strings (JEP 254, Java 9, September 2017) replaced the String's backing char[] with a byte[] plus a coder flag. ASCII and Latin-1 strings use 1 byte per character (coder LATIN1); strings with characters outside Latin-1 use 2 bytes per character (coder UTF16). This roughly halves the memory footprint of typical Java applications because most strings are ASCII. The string pool itself is unaffected -- it still deduplicates references -- but each pooled String now uses half the memory it used to.
Q7. You are loading a 500MB CSV file with millions of duplicate string cells. How do you minimise the memory used by the String objects?
Three approaches, in order of preference. First, use the G1 garbage collector with -XX:+UseStringDeduplication (JEP G1StringDedup, Java 8u20), which lets G1 deduplicate the backing arrays of equal strings without your code calling intern(); this is the most hands-off approach. Second, call intern() explicitly on each cell during parsing; this is more aggressive but adds CPU on every intern() call. Third, build a custom deduplication table (ConcurrentHashMap of String to String) and look up each cell before storing it; this gives you control over eviction. Measure before and after, because deduplication is most effective when the duplicate ratio is high.
Sample Input and Output
Input: Run StringPoolDemo on JDK 11. Output: a == b (literals): true a == c (new String): false a == c.intern(): true a == d (constant concat): true a == f (runtime concat): false a == f.intern(): true identityHashCode a: 1639645478 identityHashCode b: 1639645478 identityHashCode c: 933014898 identityHashCode f: 1702242558 Distinct interned refs: 1 Bytes in 'abc': 3
The output shows that literal strings share the same reference (a == b), new String() produces a distinct heap object (a == c is false), intern() returns the pooled reference (a == c.intern() is true), constant concat is pooled (a == d), runtime concat produces a distinct object (a == f is false) but intern() recovers the pooled reference. The identity hash codes differ between pooled and heap strings, and the interned array collapses to 1 distinct reference. The final line shows Compact Strings in action: "abc" is 3 bytes, not 6.
Common Mistakes to Avoid
First, do not use == to compare strings. It happens to work for literals but silently breaks for new String(), runtime concatenation, or strings read from a file. Always use .equals() or Objects.equals(). Second, do not call intern() on every string in a hot loop -- the hash-table lookup is not free, and in Java 6 it could exhaust PermGen. Use intern() only when you know duplicates dominate and the memory savings are worth the CPU.
Third, do not use new String("literal") -- it creates a fresh heap object that duplicates the pooled literal, wasting memory. The only valid use is to trim a large backing array (e.g., new String(hugeString.substring(0, 3)) in Java 6 where substring shared the array). Fourth, do not assume that string concatenation is always constant-folded -- only constant expressions are folded, and any non-final variable operand forces runtime concatenation. Finally, do not forget that the pool is on the heap since Java 7 -- this means interned strings can be garbage collected, which is good news but also means that an interned string is not a permanent shared singleton.
Real-World Applications
The string constant pool java is used everywhere in the JDK and in real applications. String.intern() is the basis for many caching layers: Hibernate interns entity names and field names to reduce memory in large persistence contexts. XML parsers like the JDK's built-in SAX parser intern tag names and attribute names by default, which is why repeated element names share references. The JVM itself interns the class names and method names in the constant pool of every loaded class, which is why reflection lookups are hash-table-fast.
Beyond the JDK, large-scale data processing frameworks (Spark, Flink) often deduplicate string dimensions to reduce shuffle size. Log aggregators (ELK, Splunk) deduplicate log levels and tag names. Configuration frameworks (Spring's environment abstraction) intern property keys. The G1 garbage collector's -XX:+UseStringDeduplication flag is the most popular hands-off deduplication mechanism for production Java services that read large amounts of string data from databases or files.
Conclusion
The string constant pool java is a JVM-managed hash table that deduplicates String instances, making literals share references and intern() recover pooled instances. Knowing when == works and when it fails, when concat is constant-folded and when it is runtime, and where the pool lives (the heap since Java 7) lets you write efficient string-heavy code and avoid subtle bugs. Next, study Compact Strings (JEP 254), G1 String Deduplication, and the StringBuilder/StringBuffer mutable counterparts.
Comments
Comments
Post a Comment