PS:本人学生,才疏学浅,如有错误,恳请指出。
String
的字符串常量池(String Pool
)是一个固定大小的HashTable
(数组+链表的数据结构),故不存在两个相同的字符串。也叫StringTable
。
StringTable
是放在本地内存的,是C++
写的,里面放的是字符串对象的引用,真实的字符串对象是在堆里。
像这些静态的、未加载的.class文件的数据被称为静态常量池,但经过jvm把.class文件装入内存、加载到方法区后,常量池就会变为运行时常量池。
当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,运行时常量池存在于内存中,也就是class常量池被加载到内存之后的版本。
不同之处是:它的字面量可以动态的添加(String#intern()),符号引用可以被解析为直接引用。
简单来说,HotSpot VM里StringTable是个哈希表,里面存的是驻留字符串的引用(而不是驻留字符串实例自身)。也就是说某些普通的字符串实例被这个StringTable引用之后就等同被赋予了“驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例里只有一份,被所有的类共享。类的运行时常量池里的CONSTANT_String类型的常量,经过解析(resolve)之后,同样存的是字符串的引用;解析的过程会去查询StringTable,以保证运行时常量池所引用的字符串与StringTable所引用的是一致的。
字符串常量池 |
---|
本质就是一个哈希表 |
存储的是字符串实例的引用 |
在被整个JVM共享 |
在解析运行时常量池中的符号引用时,会去查询字符串常量池,确保运行时常量池中解析后的直接引用跟字符串常量池中的引用是一致的 |
为了避免频繁的创建和销毁对象而影响系统性能,实现了对象的共享。
例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
节省运行时间:比较字符串时,==
比equals()
快。对于两个引用变量,只用==
判断引用是否相等,也就可以判断实际值是否相等。
为什么String类被设计成不可变? 传送门(点我)
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}
字面量,常量和变量之间的区别?传送门(点我)
String s1 = "hello";
此时会有如下过程。
会去解析的符号引用,ldc
指令,会先到字符串常量池中查找是否存在对应字符串实例的引用,如果有的话,那么直接返回这个字符串实例的引用,如果没有的话,会创建一个字符串实例,那么将其添加到字符串常量池中(实际上是将其引用放入到一个哈希表中),之后再返回这个字符串实例对象的引用。
ldc
:将int、float、或者一个类、方法类型或方法句柄的符号引用、还可能是String型常量值从常量池中推送至栈顶,在执行ldc
指令时会触发对它的符号引用进行解析。
问题:以下方式会创建几个对象?怎么证明?
String str = new String("hello");
2个对象或者1个对象
①如果字符串常量池中已经有“hello”,则创建了一个对象。如下图。
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = new String("hello");
System.out.println(s1==s2);
}
}
//输出:false,由图容易看出
②如果字符串常量池中不存在“hello”,则创建了两个对象。
一个对象是:new关键字在堆空间创建的
另一个对象是:另外一个是在解析常量池的时候JVM自动创建的,如下图。
如果不是用字面量的方式定义的String对象,可以使用String提供的intern方法:intern方法会从字符串常量池中查询当前字符串是否存在,若存在则返回其引用;若不存在就会将当前字符串放入常量池中,并返回其引用。我们只需牢记返回的是字符串常量池的引用(即哈希表中的值)即可。
public class Main {
public static void main(String[] args) {
String s1 = new String("1");
String s2=s1.intern();
String s3 = "1";
System.out.println(s2 == s3);
}
}
分析:true
第一行,创建了两个对象实例,其引用一个在字符串常量池中,一个返回给s,如上图。
第二行,intern()方法会会从字符串常量池中查询当前字符串是否存在,发现存在,返回的是字符串常量池的引用(地址)。
第三行,s3是赋值为字符串常量池的引用。
故 s2和s3地址一样。
String s =new String("1")+new String("1");
分析:true
执行完成后,堆区多了两个匿名对象,另外堆区还多了一个字面量为11的字符串实例,并且栈中存在一个引用指向这个实例。当我们在进行字符串拼接时,编译器默认会创建一个StringBuilder
对象并调用其append方法来进行拼接,最后再调用其toString
方法来转换成一个字符串,StringBuilder
的toString
方法其实就是new
一个字符串。
//StringBuilder的toString方法
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}