Sitemap
İbrahim Solak

İbrahim Solak

I am a recent graduate from Computer Engineering Department at Ankara University with a passion for building scalable systems and exploring modern architectures

String Literals and the String Pool in Java

8 min readMay 11, 2026

--

In Java, Strings are among the most used classes. Repeated String allocations can put significant pressure on the garbage collector. Therefore, you can probably guess that memory optimization is necessary for such a frequently used class. Fortunately, Java has an internal optimization mechanism called the String Pool (aka Intern Pool) to manage the storage of String literals.

Before diving into the core of the String Pool, we first need to understand the meaning of a “literal”.

What is a Literal ?

A literal is the source code representation of a value of a primitive type, the String type, or the null type.

public class Main {

public static void main(String[] args) {
int num = 123;
int octal = 014;
long longNum = 10000000000L;
double famous = 3.14159;
char letter = 'A';
String hello = "Hello Java";
boolean isActive = true;
String nullText = null;
}
}

These are some examples of literals for various primitive types, as well as for our main topic, the String class.

During compile-time, after optimizations, String literals are saved as records in the .class file. The actual String objects are created in the String Pool (if they do not already exist) by the JVM during run-time, when the class is loaded into memory.

What is the String Pool?

The String Pool is a dedicated region within the Heap memory where the JVM stores String instances. This mechanism ensures that identical string literals share the same memory address, reducing the overall memory footprint of an application.

Memory Location: From PermGen to Heap In Java 6 and earlier, the String Pool was located in the Permanent Generation (PermGen). This area had a fixed size, which often led to java.lang.OutOfMemoryError: PermGen space when too many strings were interned.

Starting with Java 7, the String Pool was moved to the main Heap memory. This change provided two main advantages:

1. Scalability: The pool is no longer limited by the fixed size of PermGen.

2. Garbage Collection: Strings in the pool can be reclaimed by the Garbage Collector when they are no longer referenced, improving memory management.

Now that we understand the architectural foundation of the String Pool, let’s see how the JVM handles these memory allocations in practice through a few code examples.

public class Main {

public static void main(String[] args) {
String string1 = "Java";
}
}

When the JVM checks the pool, the “Java” string does not exist, so it creates a new “Java” object in the pool.

Press enter or click to view image in full size
Diagram showing a Stack memory variable referencing a newly created ‘Java’ string object in the String Pool within the Heap memory.
public class Main {

public static void main(String[] args) {
String string1 = "Java";
String string2 = "Java";
}
}

Now, the JVM checks the pool again. Since “Java” already exists, it doesn’t create a new object. Instead, it simply assigns the existing pool object’s reference to string2.

Press enter or click to view image in full size
Diagram illustrating two Stack variables sharing the same reference to an existing ‘Java’ string object inside the JVM String Pool.
public class Main {

public static void main(String[] args) {
String string1 = "Java";
String string2 = "Java";
String string3 = new String(“Java”);
}
}

In this case, we have a string literal combined with the new keyword. The JVM checks the String Pool and sees that “Java” already exists there. However, because of the new keyword, the JVM is forced to create a brand new, separate “Java” object in the general Heap memory.

Press enter or click to view image in full size
Memory diagram comparing string literals in the String Pool versus a distinct String object created in the general Heap memory using the new keyword.

Before we continue, it is important to remember: the == operator is used to compare whether two string references point to the exact same object in memory. To compare the actual text content of two strings, the equals(Object obj) method should be used.

public class Main {

public static void main(String[] args) {
String example1 = "Hello";
String example2 = "Hello";
String example3 = new String("Hello");
String example4 = new String("Hello");

System.out.println(example1 == example2);
System.out.println(example1 == example3);
System.out.println(example3 == example4);

}
}
Output:
true
false
false

How many objects are created in the above code block ?

The answer is 3 objects.

1. The String Pool Object: When we declare example1 using a string literal, the JVM checks the String Constant Pool. Since “Hello” is not yet present, it creates the first object in the pool. For example2, the JVM finds the already existing “Hello” in the pool and simply shares its reference instead of creating a new one. Because they point to the exact same memory location, example1 == example2 returns true.

2. The First Heap Object: For example3, we explicitly use the new keyword. This forces the JVM to create a brand new, distinct “Hello” object in the general Heap memory, completely independent of the pool. Since its memory address is different from the shared pool object, example1 == example3 returns false.

Get İbrahim Solak’s stories in your inbox

Join Medium for free to get updates from this writer.

3. The Second Heap Object: The exact same rule applies to example4. The new keyword triggers the creation of yet another distinct object in the Heap. Even though the text content is identical, example3 and example4 point to completely different memory locations. Therefore, example3 == example4 also returns false.

In summary, the JVM creates one shared object in the String Pool and two independent objects in the Heap, resulting in a total of three objects.

You might wonder if there is any way to take a string created in the Heap memory (outside the String Pool) and add it to the String Pool. The intern() method does exactly this.

What is intern() ?

intern() is a method that adds a string to the String Pool if it is not already present. If the string already exists in the pool, it simply returns the reference to that existing string.

public class Main {

public static void main(String[] args) {
String string1 = new String("Java");
String string2 = "Java";
System.out.println(string1 == string2);

string1 = string1.intern();
System.out.println(string1 == string2);

}
}
Output:
false
true

In this code, the JVM first creates a new String object in the Heap memory, which is referenced by string1. At the same time, because we used the “Java” literal inside the parentheses, it ensures “Java” is also placed in the String Pool.

Next, for string2, the JVM sees the “Java” literal again. Instead of creating a new object, it simply points string2 to the existing “Java” object in the String Pool. Since string1 points to the Heap and string2 points to the Pool, the first comparison (string1 == string2) returns false.

Then, we call string1 = string1.intern();. The intern() method checks the String Pool for the text “Java”. Since it already exists in the pool, the method returns the pool’s reference. We reassign this pool reference back to string1.

Now, both string1 and string2 are pointing to the exact same object in the String Pool. As a result, the second comparison (string1 == string2) returns true.

Earlier, we mentioned that using a string literal inside the parentheses (e.g., new String(“Java”)) ensures that the string is also implicitly placed in the String Pool. You might be wondering: How can we prove this? The proof lies in how the intern() method behaves when a literal is used versus when it is not. Let’s look at the following example:

public class Main {

public static void main(String[] args) {
String string1 = new String("Apple");
System.out.println(string1 == string1.intern());

char[] letters = {'O', 'r', 'a', 'n', 'g', 'e'};
String string2 = new String(letters);
System.out.println(string2 == string2.intern());

}
}
Output:
false
true

In the first example with string1, the JVM sees the “Apple” string literal, so it places “Apple” into the String Pool. Then, because of the new keyword, it creates a separate object in the Heap referenced by string1. When we call string1.intern(), the JVM finds “Apple” already existing in the pool and returns the pool’s reference. Since string1 points to the Heap and the intern() method returns the Pool reference, string1 == string1.intern() returns false.

In the second example, there is no string literal used for “Orange”. The JVM only creates an object in the Heap using the character array. When we call string2.intern(), the JVM checks the String Pool and does not find “Orange”. Because it is missing, the JVM records string2’s Heap reference into the pool and returns that exact same Heap reference. Therefore, both point to the same object, and string2 == string2.intern() returns true.

If everything is clear up until here, it is time to get a little bit confused.

package testPackage;
class Test {
public static void main(String[] args) {
String hello = "Hello", lo = "lo";
System.out.println(hello == "Hello");
System.out.println(Other.hello == hello);
System.out.println(other.Other.hello == hello);
System.out.println(hello == ("Hel"+"lo"));
System.out.println(hello == ("Hel"+lo));
System.out.println(hello == ("Hel"+lo).intern());
}
}
class Other { static String hello = "Hello"; }
package other;
public class Other { public static String hello = "Hello"; }
Output:
true
true
true
true
false
true

String literals in the same class and package represent references to the exact same String object. Therefore, hello == “Hello” returns true.

String literals in different classes within the same package also represent references to the same String object. Thus, Other.hello == hello returns true.

String literals in different classes across different packages likewise represent references to the same String object. The String Pool is global for the JVM. So, other.Other.hello == hello returns true.

Strings concatenated from constant expressions are computed at compile-time and then treated as if they were standard literals. The compiler concatenates “Hel” and “lo” into “Hello” before the code even runs. When the JVM loads the class, it simply points to the existing “Hello” in the String Pool. This makes hello == (“Hel” + “lo”) return true.

Strings computed by concatenation at run-time (which involve variables) are newly created and therefore distinct. Because lo is a variable, the JVM dynamically creates a brand new “Hello” object in the Heap from “Hel” and lo during execution. Therefore, comparing the pool reference with the new Heap reference makes hello == (“Hel” + lo) return false.

The result of explicitly interning a computed string is the same String object as any pre-existing string literal with the exact same contents. Although (“Hel” + lo) creates a new object in the Heap, calling .intern() on it forces the JVM to check the String Pool. Since “Hello” already exists there, it returns the pool’s reference. Thus, hello == (“Hel” + lo).intern() returns true.

That concludes our look at the Java String Pool. Hopefully, the underlying mechanics of string literals and the new operator are now clear. Thank you for your time and good luck in your Java learning journey.

--

İbrahim Solak
İbrahim Solak

Written by İbrahim Solak

I am a recent graduate from Computer Engineering Department at Ankara University with a passion for building scalable systems and exploring modern architectures