字符串常量池
字符串常量池
概要
字符串常量池 是 JVM 为了提升性能和减少内存消耗,针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
一、内存区域
1)JDK1.7之前
运行时常量池(字符串常量池也在里边)是存放在方法区,此时方法区的实现是永久代。
2)JDK1.7
字符串常量池被单独从方法区移到堆中,运行时常量池剩下的还在永久代(方法区)。
说明:主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
3)JDK1.8
永久代更名为元空间(方法区的新的实现),但字符串常量池池还在堆中,运行时常量池在元空间(方法区)。
二、字符串常量池的应用
1. 堆中创建字符串对象
1 // 在堆中创建字符串对象”ab“ 2 // 将字符串对象”ab“的引用保存在字符串常量池中 3 String aa = "ab"; 4 5 // 直接返回字符串常量池中字符串对象”ab“的引用 6 String bb = "ab"; 7 8 System.out.println(aa==bb);// true
2. String s1 = new String("abc");这句话创建了几个字符串对象?
会创建 1 或 2 个字符串对象。
1) 如果字符串常量池中不存在字符串对象“abc”的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中。
示例代码(JDK 1.8):
String s1 = new String("abc");
对应的字节码为:
ldc 命令用于判断字符串常量池中是否保存了对应的字符串对象的引用,如果保存了的话直接返回,如果没有保存的话,会在堆中创建对应的字符串对象并将该字符串对象的引用保存到字符串常量池中。
2) 如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。
示例代码(JDK 1.8):
1 // 字符串常量池中已存在字符串对象“abc”的引用 2 String s1 = "abc"; 3 // 下面这段代码只会在堆中创建 1 个字符串对象“abc” 4 String s2 = new String("abc");
对应的字节码为:
说明:7 这个位置的 ldc 命令不会在堆中创建新的字符串对象“abc”,这是因为 0 这个位置已经执行了一次 ldc 命令,已经在堆中创建过一次字符串对象“abc”了。7 这个位置执行 ldc 命令会直接返回字符串常量池中字符串对象“abc”对应的引用。
三、String#intern 方法有什么作用 ?
String.intern() 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中。
可以简单分为两种情况:
1)如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
2)如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
示例代码(JDK 1.8):
1 // 在堆中创建字符串对象”Java“ 2 // 将字符串对象”Java“的引用保存在字符串常量池中 3 String s1 = "Java"; 4 // 直接返回字符串常量池中字符串对象”Java“对应的引用 5 String s2 = s1.intern(); 6 // 会在堆中在单独创建一个字符串对象 7 String s3 = new String("Java"); 8 // 直接返回字符串常量池中字符串对象”Java“对应的引用 9 String s4 = s3.intern(); 10 // s1 和 s2 指向的是堆中的同一个对象 11 System.out.println(s1 == s2); // true 12 // s3 和 s4 指向的是堆中不同的对象 13 System.out.println(s3 == s4); // false 14 // s1 和 s4 指向的是堆中的同一个对象 15 System.out.println(s1 == s4); //true
需要注意的是:调用intern会将字符串加入字符串常量池中,而字符串常量池是一个全局数据结构。滥用intern可能导致内存使用量增加,甚至可能导致内存溢出,需要谨慎使用。
四、String 类型的变量和常量做“+”运算时发生了什么?
先来看字符串不加 final 关键字拼接的情况(JDK1.8):
1 String str1 = "str"; 2 String str2 = "ing"; 3 String str3 = "str" + "ing"; 4 String str4 = str1 + str2; 5 String str5 = "string"; 6 System.out.println(str3 == str4);//false 7 System.out.println(str3 == str5);//true 8 System.out.println(str4 == str5);//false
注意:比较 String 字符串的值是否相等,可以使用 equals() 方法。 String 中的 equals 方法是被重写过的。 Object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是字符串的值是否相等。如果你使用 == 比较两个字符串是否相等的话,IDEA 还是提示你使用 equals() 方法替换。使用 == 比较字符串时,比较的是对象的引用地址,而不是字符串的内容。
对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
我们在平时写代码的时候,尽量避免多个字符串对象拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer。不过,字符串使用 final 关键字声明之后,可以让编译器当做常量来处理。
示例代码:
1 final String str1 = "str"; 2 final String str2 = "ing"; 3 // 下面两个表达式其实是等价的 4 String c = "str" + "ing";// 常量池中的对象 5 String d = str1 + str2; // 常量池中的对象 6 System.out.println(c == d);// true
被 final 关键字修饰之后的 String 会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就相当于访问常量。如果 ,编译器在运行时才能知道其确切值的话,就无法对其优化。
示例代码(str2 在运行时才能确定其值):
1 final String str1 = "str"; 2 final String str2 = getStr(); 3 String c = "str" + "ing";// 常量池中的对象 4 String d = str1 + str2; // 在堆上创建的新的对象 5 System.out.println(c == d);// false 6 public static String getStr() { 7 return "ing"; 8 }
参考链接:
https://javaguide.cn/java/basis/java-basic-questions-02.html