字符串常量池在JVM中的位置变化:
关于String以及StringBuffer、StringBuilder的相关信息可以参考博主的另一篇文章:
Java String、StringBuilder、StringBuffer类解析
String的特点:
String与字符串常量池
在Java语言中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。
常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种。
String info="atguigu.com";
Java 6及以前,字符串常量池存放在永久代
Java 7中 Oracle的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到Java堆内
public class StringTest {
public static void main(String[] args) {
System.out.println();//2152
System.out.println("1");//2153
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println("10");//2163
//如下的字符串"1" 到 "10"不会再次加载
System.out.println("1");//2163
System.out.println("2");//2163
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println("10");//2163
}
调试区域中勾选该选项可以查看String类在内存中的详情:
进行调试我们会发现,在输出10个String后,再次输出相同的字符串,而字符串常量池中没有在创建新的字符串常量对象:
//官方示例代码
class Memory {
public static void main(String[] args) {//line 1
int i = 1;//line 2
Object obj = new Object();//line 3
Memory mem = new Memory();//line 4
mem.foo(obj);//line 5
}//line 9
private void foo(Object param) {//line 6
String str = param.toString();//line 7
System.out.println(str);
}//line 8
}
intern()
方法,根据该字符串是否在常量池中存在,分为:
示例1: 常量之间的拼接会进行编译期优化
@Test
public void test() {
String s1 = "a" + "b" + "c";
String s2 = "abc";
System.out.println(s1 == s2); // true
}
@Test
public void test1() {
String s = "aabb";
final String s1 = "aa";
final String s2 = "bb";
String s3 = s1 + s2;
System.out.println(s == s3); // true
}
示例2:变量与常量、变量与变量拼接
@Test
public void test2() {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4); // false
}
此时的结果为false,这是因为在拼接过程中实现拼接功能的实际是StringBuilder对象,先创建出一个StringBuilder对象,然后调用StringBuilder中的append方法,最后调用toString方法将其转化成一个String类型的对象。所以最后s4的地址是一个String类的对象,而s3是字符串常量池当中的引用,最终结果为false。
intern()
是一种手动将字符串加入常量池中的方法,其优点是执行速度非常快,直接使用==进行比较要比使用equals()方法快很多;内存占用少。但是intern()方法每次操作都需要与常量池中的数据进行比较,查看常量池中是否存在等值数据,所以其主要适用于有限值,并且这些有限值会被重复利用的场景,这样可以减少内存消耗,同时在进行比较操作时减少时耗,提高程序性能。
String中的intern()方法是一个native方法
public native String intern();
字符串常量池池最初是空的,由String类私有地维护。在调用intern方法时,如果池中已经包含了由equals(object)方法确定的与该字符串内容相等的字符串,则返回池中的字符串地址。否则,该字符串对象将被添加到池中,并返回对该字符串对象的地址。
如果不是用双引号声明的String对象,可以使用String提供的intern方法:intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。
下面的代码中一共创建了几个对象呢?
@Test
public void test4() {
String s = new String("hello");
}
来看一下字节码指令当中的信息:
先是创建了一个String类型的对象,然后引入了常量池中的"hello",最后执行了Stirng的构造器。所以一共有两个对象产生。
@Test
public void test5() {
String s = new String("Hello") + new String("World");
}
实际上先是创建了一个StringBuilder类的对象,然后调用了StringBuilder的构造器,再从常量池中引入"Hello",创建出一个String类的对象,调用StringBuilder中的append方法将"Hello"加入,之后同样,引入"World",然后创建一个String类的对象,再次appen方法,最后调用StringBuilder中的toString方法。
为什么打印结果输出false呢?
public void test6() {
String s = new String("Hello") + new String("World");
String s2 = "HelloWorld";
System.out.println(s == s2);// false
}
这是因为StringBuilder中的toString()方法:
实际上调用了String类的构造法新建了一个String,而在这个String中只是将原来的char[]中的内容进行了复制,然后将复制的引用返回。所以toString()返回的是一个String类的对象引用,而不是常量池中的引用,所以最后结果是false。
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
//
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
打印结果是什么呢?为什么是这样的结果呢?
public class StringTest {
public static void main(String[] args) {
// 问题一:
String s = new String("1");
String s1 = s.intern();// 调用此方法之前,字符串常量池中已经存在了"1",所以返回"1"在常量池当中的引用
String s2 = "1";
System.out.println(s == s2);// jdk6:false jdk7/8:false
System.out.println(s1 == s2); // true
// 问题二:
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";// s4变量记录的地址:使用的是上一行代码代码执行时,在常量池中生成的"11"的地址
System.out.println(s3 == s4);// jdk6:false jdk7/8:true
}
}
问题一在注释中以及说明,所以重点来看问题二。
首先要明白实际在内存中的细节,才能知道为什么在jdk6中是false,而jdk6之后是true
先来看jdk6中的分析:
再来看jdk7/8中的分析:
拓展:
public class StringTest {
public static void main(String[] args) {
//执行完下一行代码以后,字符串常量池中,是否存在"11"呢?
String s3 = new String("1") + new String("1");//new String("11")
//在字符串常量池中生成对象"11",代码顺序换一下,实打实的在字符串常量池里有一个"11"对象
String s4 = "11";
String s5 = s3.intern();
// s3 是堆中的 "ab" ,s4 是字符串常量池中的 "ab"
System.out.println(s3 == s4);//false
// s5 是从字符串常量池中取回来的引用,当然和 s4 相等
System.out.println(s5 == s4);//true
}
}
代码部分:
/**
* String的垃圾回收测试:
* 加入参数:
* -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
*/
public class StringGCTest {
public static void main(String[] args) {
for (int j = 0; j < 100000; j++) {
String.valueOf(j).intern();
}
}
}
openjdk文档:http://openjdk.java.net/jeps/192
官方文档中内容节选:
许多大型 Java 应用程序目前都存在内存瓶颈。测量表明,在这些类型的应用程序中,大约 25% 的 Java 堆实时数据集被String
对象消耗。此外,这些对象中大约有一半String
是重复的,其中重复的意思 string1.equals(string2)
是正确的。在堆上拥有重复String
的对象本质上只是浪费内存。本项目将在 G1 垃圾收集器中实现自动连续String
重复数据删除,避免内存浪费,减少内存占用。
对大量 Java 应用程序(大小)进行的测量显示如下:
String
= 25%String
= 13.5%String
长度 = 45 个字符鉴于我们只对字符数组进行重复数据删除,我们仍将承担String
对象(对象头、字段和填充)的开销。此开销取决于平台/配置,在 24 和 32 字节之间变化。但是,考虑到平均String
长度为 45 个字符(90 个字节 + 数组标头),仍然有很大的优势。
考虑到上述情况,实际预期收益最终会 减少10% 左右的堆。请注意,此数字是根据广泛的应用计算得出的平均值。特定应用程序的堆减少量可能上下变化很大。
实现: