☕ Java
String Pool
The string pool (also called the string intern pool or string constant pool) is a special memory region maintained by the JVM that stores a single copy of each unique string value. When two string literals have the same content, they refer to the same object in the pool rather than two separate objects. The pool is a flyweight pattern applied at the language level — it dramatically reduces memory consumption in applications that use many repeated string values, which is nearly every application. This entry covers how the pool works, where it lives in JVM memory, how to interact with it programmatically, the intern() method, performance implications, and when to use or avoid pool entries.
How the String Pool Works
The string pool is a cache managed by the JVM that maps string content to String objects. When the JVM encounters a string literal — any string value written directly in source code — it checks whether an object with that character sequence already exists in the pool. If it does, the literal resolves to the existing pool object. If not, a new String object is created in the pool and the literal resolves to that new object. All subsequent uses of the same literal value, anywhere in the JVM, resolve to the same pool object.
This deduplication means that in any JVM, "Hello".equals("Hello") is true and "Hello" == "Hello" is also true — both literals resolve to the same pool object. The pool guarantees a single instance per unique string value for all interned strings. This is why the literal-on-the-left pattern ("hello".equals(variable)) works without null checks — the literal is guaranteed to be non-null.
The pool is populated at class loading time for string literals and at intern() call time for programmatically interned strings. String constants that appear in class files (literal values, constant concatenation like "Hello" + " World" where both operands are literals) are automatically added to the pool. Runtime strings — created by new String(), by string methods, by reading from files or network — are not automatically pooled.
Java
// ── Literal strings are automatically pooled ────────────────────────
String a = "Hello";
String b = "Hello";
System.out.println(a == b); // true — same pool object
// ── Compile-time constant concatenation is also pooled ────────────────
String c = "Hel" + "lo"; // both operands are literals → compile-time constant
System.out.println(a == c); // true — "Hello" pool entry reused
// ── Runtime strings are NOT automatically pooled ──────────────────────
String d = new String("Hello"); // explicit new — new heap object
String e = "Hel".concat("lo"); // method call — runtime result
String f = new StringBuilder("Hello").toString(); // builder result
System.out.println(a == d); // false — d is not the pool entry
System.out.println(a == e); // false — e is not the pool entry
System.out.println(a == f); // false — f is not the pool entry
// ── All are equal by content ──────────────────────────────────────────
System.out.println(a.equals(d)); // true
System.out.println(a.equals(e)); // true
System.out.println(a.equals(f)); // true
// ── intern() — force pool entry ───────────────────────────────────────
String d2 = d.intern(); // returns the pool entry for d's content
System.out.println(a == d2); // true — d2 IS the pool entry
System.out.println(d == d2); // false — d is NOT the pool entry
// ── Pool lookup semantics ─────────────────────────────────────────────
// intern() checks: is "Hello" in the pool?
// YES → return pool reference
// NO → add to pool, return new pool referenceJVM Memory Location — Where the Pool Lives
The location of the string pool in JVM memory changed significantly with Java 7. Before Java 7, the string pool resided in PermGen (Permanent Generation) — a fixed-size memory region separate from the heap. This caused OutOfMemoryError: PermGen space when applications interned too many strings, because PermGen had a fixed maximum and could not be grown at runtime.
Starting with Java 7, the string pool was moved to the main heap. This change had three important consequences. First, pool strings are now subject to garbage collection — if no other references point to a pool entry, it can be collected. Before Java 7, pool entries were permanent. Second, the pool size is now bounded by the heap size, not by a fixed PermGen limit, making OutOfMemoryError from pooling much harder to trigger. Third, the -XX:StringTableSize JVM flag controls the number of buckets in the pool's hash table, defaulting to 65536 in Java 11+. A larger table means fewer collisions and faster intern() lookups.
Java 8 eliminated PermGen entirely, replacing it with Metaspace (which stores class metadata but not string pool data). Understanding this history explains why some pre-Java-7 advice about limiting intern() usage is outdated — the JVM has become much better at managing the pool.
Java
// ── Pool location changed in Java 7 ─────────────────────────────────
//
// Java 6 and before:
// String Pool → PermGen (fixed size, ~64MB default, no GC)
// Risk: OutOfMemoryError: PermGen space with many interned strings
//
// Java 7+:
// String Pool → Heap (normal heap, subject to GC)
// Risk: essentially eliminated for normal use cases
//
// Java 8+:
// PermGen eliminated entirely
// String Pool → Heap (same as Java 7)
// Class metadata → Metaspace (growable, not related to pool)
// ── Pool strings ARE garbage collected in Java 7+ ─────────────────────
// If the only reference to a pool entry is the pool itself (no other vars)
// AND the GC determines it's unreachable, it CAN be collected
// In practice this is rare but theoretically possible
// ── JVM flags for pool tuning ─────────────────────────────────────────
// -XX:StringTableSize=131072 (larger hash table → fewer collisions)
// Default: 65536 buckets in Java 11+, 1009 in Java 6, 60013 in Java 7-10
// Larger table reduces collision chains → faster intern() lookup
// ── Monitoring pool usage with JVM tools ─────────────────────────────
// jcmd <pid> VM.stringtable
// Output:
// StringTable statistics:
// Number of buckets : 65536 = 524288 bytes, each 8
// Number of entries : 39127 = 626032 bytes, each 16
// Number of literals : 39127 = 4316408 bytes, [...]
// Average bucket size : 0.597
// Variance of bucket size : 0.603
// ── G1GC string deduplication (Java 8u20+) ───────────────────────────
// -XX:+UseG1GC -XX:+UseStringDeduplication
// G1 GC identifies duplicate String objects on the heap (NOT just pool)
// and makes them share the same backing char[]/byte[] array
// Different from intern() — the String objects are still separate,
// but their internal character arrays are deduplicated
// Useful when many equal strings exist that are NOT internedThe intern() Method
The intern() method explicitly adds a string to the pool (or retrieves the existing pool entry if the content already exists). Calling s.intern() on a String s checks whether the pool contains an entry equal to s. If it does, intern() returns the existing pool entry without creating any new object. If it does not, intern() adds s to the pool and returns s itself.
The primary use case for intern() is reducing memory when a program handles a large number of strings from external sources (files, databases, network) where many strings have the same content. Interning these strings means only one object exists per unique value, dramatically reducing memory. The cost is the intern() call itself — it performs a hash lookup and possibly a hash table insertion, which is a constant-time operation but not free.
Over-use of intern() can cause problems. If the strings being interned are genuinely unique (every string is different, like UUIDs), interning wastes time with zero memory benefit. If the number of unique interned strings is very large, the pool's hash table can become large. Modern advice is to use intern() selectively — for frequently repeated string values from external sources — rather than indiscriminately. In most application code, explicit intern() is unnecessary because the JVM handles literals automatically.
Java
// ── intern() semantics ───────────────────────────────────────────────
String s1 = new String("Hello"); // heap object, NOT in pool
String s2 = new String("Hello"); // another heap object, NOT in pool
System.out.println(s1 == s2); // false — two separate objects
System.out.println(s1.intern() == s2.intern()); // true — same pool entry
// ── intern() returns the canonical instance ───────────────────────────
String literal = "Hello"; // pool entry
String fromNew = new String("Hello");
String interned = fromNew.intern(); // returns existing pool entry
System.out.println(literal == interned); // true — same pool object
System.out.println(fromNew == interned); // false — fromNew != pool entry
// ── Use case: deduplicating strings from external source ──────────────
// Imagine reading 1,000,000 records where only 1,000 unique country codes
// Without intern: 1,000,000 String objects on heap
// With intern: 1,000 pool entries + 1,000,000 references to them
List<String> records = new ArrayList<>();
for (String line : readLinesFromFile()) {
String countryCode = parseCountryCode(line).intern(); // deduplicate
records.add(countryCode);
}
// Memory: only one String object per unique country code
// ── Benchmark: intern() lookup cost ───────────────────────────────────
long start = System.nanoTime();
for (int i = 0; i < 1_000_000; i++) {
String s = ("value" + (i % 100)).intern(); // 100 unique strings
}
System.out.println("1M intern() calls: " +
(System.nanoTime() - start) / 1_000_000 + "ns avg");
// Typically 100-500ns per call — cheap but not free
// ── When NOT to use intern() ──────────────────────────────────────────
// DON'T: intern UUID strings — all unique, no deduplication benefit
String id = UUID.randomUUID().toString().intern(); // wasteful
// DON'T: intern in tight inner loops unless profiling confirms benefit
// DO: intern when reading many repeated string values from external sources
// DO: use equals() not == for comparison — intern() isn't needed for correctnessPool Behaviour with String Operations
Understanding which String operations produce pool entries and which produce new heap objects clarifies when == will or will not work and when intern() might be worthwhile. The rules are: string literals and compile-time constant expressions are always in the pool; runtime-computed strings (from methods, constructors, readers) are never automatically in the pool.
Several String methods deserve specific attention. substring(), trim(), strip(), toLowerCase(), toUpperCase(), replace(), and all other String methods return new String objects that are not in the pool, even when the result happens to equal a literal. Calling intern() on the result would retrieve the pool entry if one exists, but this is rarely necessary — equals() works correctly without it.
String.valueOf() and various toString() methods produce heap strings, not pool entries. Integer.toString(42) is not the same object as "42", but Integer.toString(42).intern() would retrieve the pool entry for "42" if it exists. In practice, the distinction only matters when using == intentionally to compare canonical instances.
Java
// ── Which operations produce pool entries ────────────────────────────
//
// POOL entries (use == safely):
String lit1 = "hello";
String lit2 = "world";
String concat_const = "hello" + "world"; // compile-time constant
final String F = "hel";
String final_concat = F + "lo"; // final var → compile-time constant
// NOT pool entries (use equals(), NOT ==):
String a = "hello";
String method_result = a.substring(0); // new object (even if content same)
String trim_result = " hello ".trim(); // new object
String upper = "hello".toUpperCase(); // new object
String replace_result = "hello".replace('l','l');// new object (even no change!)
String valueOf_result = String.valueOf(42); // new object
String concat_runtime = a.concat(" world"); // new object
System.out.println("hello" == method_result); // false — even though same content
System.out.println("hello".equals(method_result)); // true — correct comparison
// ── Java 11+ strip() — same as trim() for pooling purposes ────────────
String stripped = " hello ".strip();
System.out.println("hello" == stripped); // false
System.out.println("hello".equals(stripped)); // true
// ── final variables and compile-time constants ────────────────────────
final String PREFIX = "Hello";
final String SUFFIX = " World";
String combined = PREFIX + SUFFIX; // both final → compile-time constant
String literal = "Hello World";
System.out.println(combined == literal); // true — same pool entry
String prefix = "Hello"; // NOT final
String notConstant = prefix + " World"; // runtime concat
System.out.println(notConstant == literal); // false
// ── switch on String uses pool-aware comparison ───────────────────────
// The Java switch statement on strings uses equals() internally,
// not ==, so it works correctly regardless of pool membership
String input = new String("start"); // not in pool
switch (input) {
case "start" -> System.out.println("Starting!"); // works correctly
case "stop" -> System.out.println("Stopping!");
}Related Topics in Strings
String Class
String is one of the most fundamental classes in Java — used in virtually every program, yet deeply misunderstood by many developers. A String represents an immutable sequence of Unicode characters. It is not a primitive type but a full class in java.lang, automatically imported into every Java file. Understanding String means understanding how it is stored in memory, why it is immutable, how the string pool works, what the difference between == and equals() means for strings, and how to use the class efficiently. This entry covers String's nature as a class, its internal representation, the critical distinction between reference equality and value equality, String's place in the type hierarchy, and the design decisions that make String behave the way it does.
Immutable String
String immutability is the most important design decision in Java's String class. Once a String object is created, its character sequence can never change. No method on String modifies the string; every method that appears to modify returns a new String object containing the result. This design decision drives thread safety, enables the string pool, makes strings safe hash map keys, and simplifies reasoning about string values. Understanding why String is immutable, how immutability is enforced, and what the consequences of immutability are clarifies the behaviour of virtually every piece of Java code that handles strings.
Mutable String
Java provides two mutable string classes for scenarios where String's immutability would be inefficient: StringBuilder and StringBuffer. Both maintain an internal character buffer that can be modified in place — characters can be appended, inserted, deleted, and replaced without creating new objects. StringBuilder is the modern choice for single-threaded use; StringBuffer is the legacy thread-safe version with synchronised methods. This entry covers the internal buffer mechanics, the full API of both classes, performance characteristics, when to use each, thread safety implications, and the complete patterns for efficient string construction.
String Methods
The String class provides over 60 methods covering character inspection, searching, comparison, transformation, splitting, joining, formatting, and encoding. Knowing these methods well eliminates the need to write manual character-by-character loops for common string operations and prevents the common mistake of reimplementing logic that the library already provides efficiently. This entry covers every major method category with precise semantics, edge cases, performance characteristics, and practical examples — from the fundamental length() and charAt() through to the modern Java 11+ additions like strip(), isBlank(), lines(), and repeat().