Topic 6.1
The String Pool and Immutability
In one line
A String object can never change after it's created, which lets the JVM keep one shared copy of each string literal in the string pool. Every method that seems to change a String returns a new one, and intern() lets you put run-time Strings into the pool too.
Think of it like this
A school library with one copy of each printed poster. When two classes ask for the "Fire Exit" poster, the librarian doesn't print two; she points both classes to the same poster on the wall, because nobody is allowed to scribble on it. If a class wants a different poster, they must ask for a new one to be printed. Java's string pool is that wall of shared posters, and Strings are immutable (no scribbling allowed), which is the only reason sharing is safe.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Immutable
- Can't be changed after it's created. To get a different value you make a new object.
- String literal
- Text written directly in your code between double quotes, like
"hello". - String pool
- A table the JVM keeps so that each distinct literal text exists as just one shared String object.
- Intern
- To put a String into the pool, or get the pooled copy of it.
s.intern()does this. - Compile-time constant
- A value the compiler can work out before the program runs, such as
"ab" + "c"or2 * 3. - Constant variable
- A
finalvariable of a primitive orStringtype, initialised with a compile-time constant. The compiler can replace it with its value. - Heap
- The area of memory where all objects live (Topic 3.11). The garbage collector frees objects there that nothing points to any more.
- StringTable
- The JVM's internal hash table that remembers which String objects are in the pool.
Step by step
01Two literals, one object
When javac compiles String a = "chai";, it stores the text chai once in the class file's constant pool (a table of constants inside every .class file). The bytecode instruction ldc ("load constant") loads it.
The first time ldc runs for that entry, the JVM looks for chai in the StringTable. If it's not there, it creates a String object on the heap, records it in the table and returns it. The next ldc of the same text, from any class, finds it and returns the same reference. That's why a == b is true for two identical literals.
String a = "chai";
String b = "chai";
System.out.println(a == b); // true: both point to the one pooled object02new String always makes a new object
new String("chai") does two things: the literal "chai" is pooled as usual, and then new allocates a second, separate String object with the same characters. c == a is false, while c.equals(a) is true.
There's almost never a reason to write new String("..."). It wastes an object. The constructor exists for converting other things, like new String(bytes, StandardCharsets.UTF_8) (Topic 6.8) or new String(charArray).
03Constant folding: the compiler joins literals for you
If every part of a + expression is a compile-time constant, javac computes the result while compiling. "ch" + "ai" appears in the class file as the single literal "chai", so it's pooled and == to "chai".
A final String part = "ch"; is a constant variable, so part + "ai" folds too. A plain (non-final) variable is not: nonFinal + "ai" runs at run time (through StringConcatFactory since Java 9) and makes a new heap String.
Only final locals or fields initialised with a constant count. final String p = someMethod(); is final but not constant, so nothing folds.
final String part = "ch";
String e = part + "ai"; // folded to "chai" by javac: pooled
String nonFinal = "ch";
String f = nonFinal + "ai"; // computed at run time: a new object
System.out.println(e == "chai"); // true
System.out.println(f == "chai"); // false04intern(): put a run-time String in the pool
s.intern() returns the pooled String with the same characters as s. If none exists yet, it adds s itself to the pool. Either way, after x = x.intern(), x refers to the canonical object, and == against the literal works.
Interning trades CPU (a hash-table lookup every call) for memory (duplicates collapse into one object). It can pay off when a program keeps millions of copies of a few repeated values, such as country codes read from a file. In everyday code, don't intern: compare with equals (Topic 6.2) and let the garbage collector handle duplicates.
05Why the characters can never change
Look at the JDK source for java.lang.String (Java 9+): public final class String with private final byte[] value;, private final byte coder; and private int hash;. The class is final, so no subclass can add a setter. The array field is private and final, and the class never hands that array out: toCharArray() and getBytes() return copies.
final on the field only stops the reference from being reassigned; it's the class's discipline (never writing into the array after construction) that makes the contents immutable. This is the same recipe you used in Topic 4.11 for your own immutable classes.
Trying to assign a character fails at compile time, because charAt returns a value, not a variable you can write to.
06"Changing" methods return a new String
s.toUpperCase() returns a new String; s is unchanged. If you ignore the return value, nothing happens at all. That's the most common String bug in beginner code: name.trim(); on its own line.
Some methods return the same object when there's nothing to do. trim() documents this: it returns "this string if it has no leading or trailing space". That's safe only because Strings are immutable; sharing a mutable object like that would be a bug.
String name = " Asha ";
name.trim(); // result thrown away: name is still " Asha "
name = name.trim(); // keep the new String: name is now "Asha"07Inside the object: compact strings and the cached hash
Up to Java 8, a String held a char[], 2 bytes per character. Since Java 9 (JEP 254, compact strings), it holds a byte[] plus a coder flag: if every character fits in Latin-1 (codes 0 to 255), each one takes 1 byte; otherwise the whole String uses UTF-16, 2 bytes per char. English-heavy apps saw large heap savings with no code change.
The hash field starts at 0 and is filled in the first time hashCode() is called. After that, hashCode() is a field read. That's another payoff of immutability: a cached hash could never go stale.
substring copies the characters it needs (since Java 7 update 6). Before that, a substring shared the big parent array, so a tiny substring of a huge String could keep megabytes alive; that leak is gone.
Try it yourself
- 1
Predict, then run
In the first example, remove
finalfrompart. Before running, predict the lineconstant concat == a. Then run it and explain the answer using the words constant variable and run time. - 2
Intern a String you build yourself
Build
String g = new StringBuilder("ch").append("ai").toString();, printg == a, theng.intern() == a. Then printg == g.intern(). Can you explain why the last one isfalse? (Hint:"chai"was already in the pool from the literal.) - 3
Watch a method ignore you
In the second example, add
s.replace('H', 'J');on its own line before the final prints, and prints. Predict whetherschanges. Then fix it so it does.
Code & diagrams
Never rely on == for real comparisons. This program only uses it to reveal which variables share one object.
Expected output
a == b: true
a == c: false
a.equals(c): true
a == c.intern(): true
constant concat == a: true
runtime concat == a: false
runtime concat, interned == a: trueExpected output
after ignoring the result: hello
s: HELLO, t: hello
u: hello world, t: hello
trim() with nothing to trim returns the same object: true
trim() with spaces returns a new object: trueThis is why security APIs such as Console.readPassword return char[]: you can overwrite it, while a String lingers until garbage collected.
Expected output
String after shout: quiet
char[] after shout: [Q, U, I, E, T]
wiped password: ******# Print StringTable statistics when the JVM exits
java -XX:+PrintStringTableStatistics Main
# Live statistics of a running JVM (pid 12345)
jcmd 12345 VM.stringtable
# Number of buckets in the StringTable (bigger = fewer collisions when interning a lot)
java -XX:StringTableSize=1000003 Main
# G1/other collectors: share identical backing arrays of duplicate Strings
java -XX:+UseG1GC -XX:+UseStringDeduplication MainBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Try to change a character in place
Write s.charAt(0) = 'J'; to turn "java" into "Java".
Break #2
Treat a String like an array
Write s[0] = 'J';, as you might in C.
Break #3
Ignore the returned String
Call name.trim(); and name.toUpperCase(); on their own lines, then print "[" + name + "]".
Myth vs fact
Myth
new String("x") puts the String in the pool.
Fact
The literal "x" is pooled, but new always creates an extra, unpooled object. Only literals, constant expressions and intern() results are pooled.
Myth
The string pool lives in PermGen and can fill up.
Fact
That was true up to Java 6. Since Java 7 pooled Strings are normal heap objects, and since Java 8 PermGen doesn't exist at all. Unreferenced interned Strings are garbage collected.
Myth
Interning everything saves memory, so it's good practice.
Fact
Interning costs a hash-table lookup on every call, and the duplicates you'd remove are often short-lived anyway. Use it only after measuring that many long-lived duplicates exist, or use G1's -XX:+UseStringDeduplication, which shares backing arrays automatically.
Myth
final String s makes the text immutable.
Fact
Strings are immutable with or without final. final only stops the variable from pointing to a different String later.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Literal resolution is lazy: the String for an
ldcis created (and interned) the first time that instruction runs, not when the class file is parsed. The JLS (§3.10.5) guarantees that literals and constant expressions with equal contents are the same object across all classes and packages. - ▸
String deduplication (
-XX:+UseStringDeduplication, G1 since 8u20, and other collectors since Java 18) is different from interning: it makes duplicate String objects share onebyte[], but the String objects stay distinct, so==results don't change. It runs in the background on Strings that survive a few GCs. - ▸
Compact strings mean
charAtmust checkcoderfirst; the JIT turns most String methods into intrinsics, so the check is cheap. You can switch the feature off with-XX:-CompactStrings, but there's rarely a reason. - ▸
The cached hash is computed lazily and written without synchronisation. That's a safe benign race: every thread computes the same value from immutable data. Java 13 added a
hashIsZeroflag so a String whose real hash is 0 isn't recomputed every call.
Remember this
- 1
A
Stringis immutable: once the object exists, its characters can never change. The class isfinal(no subclass can add a way to change it), its internal array isprivate final, and no method writes to that array after construction. Methods liketoUpperCase(),replace(),trim()andconcat()build and return a new String, or return the same object when there is nothing to change. The original is always left exactly as it was. - 2
The string pool (also called the string constant pool or intern pool) is a table the JVM keeps of interned Strings: at most one String object per distinct sequence of characters. Every string literal in your code (
"chai") and every compile-time constant expression ("ch" + "ai") is interned automatically when the class is loaded and the literal first used, so all identical literals in a program, even across different classes, refer to the same object. - 3
new String("chai")always creates a new object on the heap, separate from the pooled one, even though the characters are the same. Strings built at run time (by concatenating variables, reading a file, callingsubstring) are also ordinary heap objects that are not in the pool. Callings.intern()looks up the pool: if an equal String is already there it returns that pooled object; otherwise it addssto the pool and returnss. - 4
Compile-time constants matter here.
"ch" + "ai"is folded byjavacinto the single literal"chai", so it is pooled. Afinallocal variable initialised with a literal is also a constant variable, sopart + "ai"withfinal String part = "ch"is folded too. Removefinaland the same expression is computed at run time, producing a brand-new, unpooled String. - 5
Where does the pool live? Since Java 7, the pooled String objects are ordinary objects on the heap (before that they lived in a fixed-size area called PermGen, which caused
OutOfMemoryError: PermGen spacein apps that interned a lot). The JVM's lookup table, called the StringTable, is a hash table in native memory that holds references to those heap objects. Unused interned Strings can be garbage collected like any other object. - 6
Immutability buys four big things. Safety: a String passed to a method, used as a file name or a class name, can't be changed behind your back. Sharing: the pool and
substringresults can be reused freely. Hash caching: a String computes itshashCodeonce and stores it in a field, which makes Strings fastHashMapkeys (Phase 9). Thread safety: any number of threads can read the same String with no locking (Phase 13). The price is that "changing" text creates new objects, which is whyStringBuilderexists (Topic 6.3).
Explain it without notes
What is the string pool, what goes into it, and where does it live in a modern JVM?
Why is String immutable? Give at least three benefits.
Explain why "a" + "b" == "ab" is true but x + "b" == "ab" (with String x = "a";) is false.
What does intern() do, and when would you use it?
How many String objects can String s = new String("hello"); create?
Practice
Write a program that builds the String "tea" in three ways (a literal, new String, and concatenation of a non-final variable) and prints, for each, whether it is == to the literal and whether it is equals to it.
Write a method static String capitalise(String s) that returns s with its first letter in upper case. Show that the caller's String is unchanged after the call.
Write a program with a final constant and a non-final variable that proves which concatenations the compiler folds. Print four labelled == results.
Trade-offs
- ↔
Immutability makes Strings safe to share, cache and use as keys, but every edit allocates a new object. For a few edits that's fine; for building text in a loop, use
StringBuilder(Topic 6.3). - ↔
Interning saves memory when there are many long-lived duplicates, but costs a lookup per call and adds pressure on the StringTable. Measure first; G1 string deduplication often gets most of the benefit with no code change.
- ↔
Holding secrets in a
Stringis convenient but they can't be wiped and may sit in memory (and heap dumps) until GC. Security-sensitive APIs usechar[]so callers can clear it after use.
Done when you can
Done when you can explain what the string pool is and what gets into it automatically.
Done when you can predict
==results for literals,new String, constant folding and run-time concatenation.Done when you can explain what
intern()returns and when it's worth using.Done when you never ignore the return value of a String method.
Done when you can list the benefits of String immutability and how the class enforces it.
Done when you can describe compact strings and the cached hash code.