Command Palette

Search for a command to run...

PHASE 6Intermediate ~32 min· topic 1 of 8

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" or 2 * 3.
Constant variable
A final variable of a primitive or String type, 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.

Main.javawhole filejava
String a = "chai";
String b = "chai";
System.out.println(a == b);   // true: both point to the one pooled object
Two literals, one objectdiagram
Rendering diagram…

02new 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).

terminal
$ javap -c Main
── expected output ──
0: ldc #7 // String chai
2: astore_1
3: new #9 // class java/lang/String
6: dup
7: ldc #7 // String chai
9: invokespecial #11 // Method java/lang/String."<init>":(Ljava/lang/String;)V
12: astore_2

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.

Main.javawhole filejava
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");   // false

04intern(): 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.

terminal
$ javac Main.java
── expected output ──
Main.java:4: error: unexpected type
s.charAt(0) = 'J';
^
required: variable
found: value
1 error

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.

Main.javawhole filejava
String name = "  Asha  ";
name.trim();                 // result thrown away: name is still "  Asha  "
name = name.trim();          // keep the new String: name is now "Asha"
"Changing" methods return a new Stringdiagram
Rendering diagram…

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. 1

    Predict, then run

    In the first example, remove final from part. Before running, predict the line constant concat == a. Then run it and explain the answer using the words constant variable and run time.

  2. 2

    Intern a String you build yourself

    Build String g = new StringBuilder("ch").append("ai").toString();, print g == a, then g.intern() == a. Then print g == g.intern(). Can you explain why the last one is false? (Hint: "chai" was already in the pool from the literal.)

  3. 3

    Watch a method ignore you

    In the second example, add s.replace('H', 'J'); on its own line before the final prints, and print s. Predict whether s changes. Then fix it so it does.

Code & diagrams

Pooled literals, new String and intern() New tab

Never rely on == for real comparisons. This program only uses it to reveal which variables share one object.

Sign in to run this example in your browser.

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: true
Strings never change: methods return new ones New tab
Sign in to run this example in your browser.

Expected 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: true
Why immutability makes Strings safe to share New tab

This is why security APIs such as Console.readPassword return char[]: you can overwrite it, while a String lingers until garbage collected.

Sign in to run this example in your browser.

Expected output

String after shout: quiet
char[] after shout: [Q, U, I, E, T]
wiped password: ******
Inspecting the pool (JVM flags)bash
# 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 Main

Break 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".

terminal
$ javac Main.java
── what you'll see ──
Main.java:4: error: unexpected type
s.charAt(0) = 'J';
^
required: variable
found: value
1 error

Break #2

Treat a String like an array

Write s[0] = 'J';, as you might in C.

terminal
$ javac Main.java
── what you'll see ──
Main.java:4: error: array required, but String found
s[0] = 'J';
^
1 error

Break #3

Ignore the returned String

Call name.trim(); and name.toUpperCase(); on their own lines, then print "[" + name + "]".

terminal
$ java Main.java
── what you'll see ──
[ asha ]

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 ldc is 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 one byte[], 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 charAt must check coder first; 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 hashIsZero flag so a String whose real hash is 0 isn't recomputed every call.

Remember this

  1. 1

    A String is immutable: once the object exists, its characters can never change. The class is final (no subclass can add a way to change it), its internal array is private final, and no method writes to that array after construction. Methods like toUpperCase(), replace(), trim() and concat() 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. 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. 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, calling substring) are also ordinary heap objects that are not in the pool. Calling s.intern() looks up the pool: if an equal String is already there it returns that pooled object; otherwise it adds s to the pool and returns s.

  4. 4

    Compile-time constants matter here. "ch" + "ai" is folded by javac into the single literal "chai", so it is pooled. A final local variable initialised with a literal is also a constant variable, so part + "ai" with final String part = "ch" is folded too. Remove final and the same expression is computed at run time, producing a brand-new, unpooled String.

  5. 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 space in 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. 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 substring results can be reused freely. Hash caching: a String computes its hashCode once and stores it in a field, which makes Strings fast HashMap keys (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 why StringBuilder exists (Topic 6.3).

Explain it without notes

01

What is the string pool, what goes into it, and where does it live in a modern JVM?

02

Why is String immutable? Give at least three benefits.

03

Explain why "a" + "b" == "ab" is true but x + "b" == "ab" (with String x = "a";) is false.

04

What does intern() do, and when would you use it?

05

How many String objects can String s = new String("hello"); create?

Practice

01

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.

02

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.

03

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 String is convenient but they can't be wiped and may sit in memory (and heap dumps) until GC. Security-sensitive APIs use char[] 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.