String Fundamentals and Internal Structure
The String class in Java is declared as final, meaning it cannot be extended. It implements both Serializable and Comparable interfaces, enabling it to support serialization and comparison operations. Prior to Java 8, strings were internally represented using a char[]. Starting from Java 9, this was changed to a byte[] along with an encoding flag to optimize memory usage.
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
@Stable
private final byte[] value;
}
This change allows Java to use more compact representations for strings. For example, Latin-1 encoded characters occupy only one byte, while UTF-16 characters (including most Unicode characters) use two bytes per character.
Immutability of Strings
Strings in Java are immutable, meaning any operation that appears to modify a string actually creates a new String object. Consider the following code:
@Test
public void testImmutability() {
String s1 = "abc";
String s2 = "abc";
s1 = "hello";
System.out.println(s1 == s2); // false
System.out.println(s1); // hello
System.out.println(s2); // abc
}
When s1 is reassigned to "hello", a new object is created in the string pool, and s1 now references this new object, leaving the original unchanged.
String Constant Pool and Hashtable Internals
The string constant pool is implemented as a fixed-size hashtable with a default size of 1009. In Java 6 and earlier, this size was fixed and could not be changed. Starting from Java 7, the default size was increased to 60013, and in Java 8, 1009 became the minimum allowed size.
Excessive strings in the pool can lead to hash collisions, which degrade performance, especially during intern() calls. You can adjust the size of the string pool using the -XX:StringTableSize JVM option.
Memory Allocation and Constant Pool
Strings, like primitive types, benefit from a caching mechanism known as the constant pool. This pool is managed by the JVM and helps reduce memory usage by reusing existing string instances.
- Java 6 and earlier: The constant pool resides in the permanant generation.
- Java 7 and later: The constant pool is moved to the Java heap, improving garbage collection and memory tuning flexibility.
Using String.intern() can be beneficial in Java 7 and later for optimizing memory usage when dealing with many duplicate strings.
String Concatenation Behavior
String concatenation behavior depends on whether the operands are constants or variables:
- Concatenation of constants is optimized at compile time.
- Concatenation involving variables uses
StringBuilderat runtime. - If a concatenation result is passed to
intern(), it may be added to the constant pool.
@Test
public void testConcatenation() {
String s1 = "a" + "b" + "c"; // compile-time optimization
String s2 = "abc";
System.out.println(s1 == s2); // true
String s3 = "javaEE";
String s4 = s3 + "hadoop"; // runtime concatenation
System.out.println(s4 == "javaEEhadoop"); // false
}
Efficiency of StringBuilder
Repeated concatenation using the + operator is inefficient due to the creation of intermediate StringBuilder and String objects. Using StringBuilder directly is significantly more efficient:
@Test
public void testEfficiency() {
long start = System.currentTimeMillis();
method1(100000); // ~4000ms
long end = System.currentTimeMillis();
System.out.println("Time: " + (end - start));
}
public void method1(int count) {
String s = "";
for (int i = 0; i < count; i++) {
s += "a"; // creates new objects each iteration
}
}
public void method2(int count) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) {
sb.append("a");
}
}
Interning and Memory Optimization
The intern() method allows explicit control over the string constant pool. It ensures that only one copy of a particular string exists in memory, which is especially useful in applications handling large volumes of duplicate strings.
@Test
public void testInterning() {
String s1 = new String("ab").intern();
String s2 = "ab";
System.out.println(s1 == s2); // true
}
In Java 6, intern() copies the string into the permanent generation if not already present. From Java 7 onward, it stores a reference to the heap string in the constant pool, avoiding duplication.
Garbage Collection of String Pool
Starting with Java 8, since the string pool resides in the heap, it is subject to regular garbage colection. This helps reclaim memory from unused interned strings, improving memory efficiency.
// Enable string pool statistics and GC details
// -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
public class StringGCTest {
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
String.valueOf(i).intern();
}
}
}
String Deduplication in G1 Garbage Collector
Starting from Java 8, the G1 garbage collector supports automatic string deduplication to reduce memory footprint. This feature is enabled using the following JVM options:
-XX:+UseStringDeduplication– Enables deduplication.-XX:+PrintStringDeduplicationStatistics– Prints deduplication statistics.-XX:StringDeduplicationAgeThreshold– Sets the age threshold for deduplication eligibility.
Deduplication works by identifying duplicate character arrays and replacing them with references to a shared array, reducing memory usage significantly in applications with many repeated strings.