一、String的不可变性
咱们先来看一下String类的声明
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
首先知道的是String实际上内部维护的是char数组,并且这个数组和String一样,都用final修饰,证明String是不可变的和不可被继承的,进一步解释就是一旦String对象被创建,那么内部的成员变量的值包括基本数据类型都不能被改变,不能指向其他对象,指向的对象的状态也不能被改变,那么这样设计的好处有什么呢?原因有以下
- 只有当String是不可变时,String常量池才有可能实现并且为heap节省了空间
- 网络安全,否则黑客可以改变String指向的对象的值而造成安全漏洞问题
- 线程安全,可以被多个线程共享
- 性能,因为String不可变,所以String创建时的hashcode也具有唯一性,作为Map的键时比其他键对象快
二、String Constant Pool(String常量池)
JVM为了提升性能和减少内存开销,避免字符串的重复创建,维护了一块特殊的内存空间,即String Pool(字符串池)
常量池底层方法是 String#intern() 使用StringTable数据结构保存字符串引用,StringTable是一个固定大小的Hashtable,默认大小是1009。基本逻辑与Java中HashMap相同,也使用拉链法解决碰撞问题。既然是拉链法,那么如果放进的String非常多,就会加剧碰撞,导致链表非常长。最坏情况下, String#intern() 的性能由O(1)退化到O(n)。
深入解析String#intern
https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html
在JDK6的版本中,String Pool使用固定容量的HashMap实现并存储在永久代中的,后面变为可配置,因为永久带内存有限,所以在JDK7开始就移动到heap(堆内存)中,这就意味着你可以通过调整堆大小来调整应用程序,通过JVM参数-XX:StringTableSize可以调整String常量池的大小(质数),同样的Size,处理的量越大就越慢,不同的Size,越大性能越好
创建字符串对象的方式有两种
- 通过字面常量赋值
- 通过new关键字新建字符串对象
这两种方式在性能和内存占用上存在差别,下面来看一下这两种方式还有其他的一些情况下JVM中发生了什么
1.字面常量赋值
String s1 = "abc";
String s2 = "ab"+"c";
System.out.println(s1 == s2); //true
常量折叠:这里穿插一个概念,由于编译期的优化,对于用"+"连接的字面常量会在编译器直接并起来.比如上例的
String s2 =“ab”+“c”;会在编译器被优化成 String s2 = “abc”;
采用字面常量去创建一个字符串时,JVM会在运行时常量池寻找有没有该字符串,有则直接返回常量池中的引用,没有就直接在常量池中创建该字符串,然后返回引用。所以上例的s1和s2指向的都是同一个对象,用 == 比较就会返回true,我们也可以通过字节码来进一步确认
0 ldc #2 <abc>
2 astore_1
3 ldc #2 <abc>
5 astore_2
6 return
当调用ldc #2,如果 #2 的symbol还没解析,则会调用C++底层的 StringTable::intern 方法生成char数组,并将引用保存在 StringTable和常量池中,当下次调用 ldc #2,通过将常量池中 #2对应的字符串推送到栈顶获取到 “abc”,避免再次到StringTable中查找。astore_1 将 “abc” 保存到 局部变量
2.使用new关键字新建
String s3 = new String("abc");
String s4 = "abc";
System.out.println(s3 == s4); //false
我们来分析一下发生了什么
①因为"abc"是用字面常量定义了,所以JVM会在运行时常量池中寻找,有则进入②,没有则创建然后进入②
②由于使用了new,所以JVM会在 heap(堆) 中创建一个内容相同的String对象,然后返回堆中Sring对象的引用
下面是字节码
0 new #2 <java/lang/String>
3 dup
4 ldc #3 <abc>
6 invokespecial #4 <java/lang/String.<init>>
9 astore_1
10 return
所以,分别在常量池和堆中生成了两个内容相同的String对象
3.使用变量连接的情况
String s5 = "ab";
String s6 = s5 + "c";
重点在s6,因为s5是一个变量,即使我们知道这个值,但是Jvm仍然认为这是一个变量,所以在编译期,这个值是未知的。在运行期,JVM就在 heap(堆) 中创建了一个内容为"abc"的对象并返回给s6,而"ab"和"c"是以字面常量的形式定义的,所以会在常量池中出现.
下面是字节码
0 ldc #2 <ab>
2 astore_1
3 new #3 <java/lang/StringBuilder>
6 dup
7 invokespecial #4 <java/lang/StringBuilder.<init>>
10 aload_1
11 invokevirtual #5 <java/lang/StringBuilder.append>
14 ldc #6 <c>
16 invokevirtual #5 <java/lang/StringBuilder.append>
19 invokevirtual #7 <java/lang/StringBuilder.toString>
22 astore_2
23 return
字符串变量的连接动作,在编译期会被转化成StringBuilder的append操作
4.使用final关键字修饰String
final String s7 = "ab";
String s8 = s7 + "c";
在这种情况下,final修饰的s7被视为一个常量,所以常量池里会有"ab",s7在编译期已经是确定了,所以s7+“c"连接后的字符串s8会在常量池中出现,也就是"abc”
下面是字节码
0 ldc #2 <ab>
2 astore_1
3 ldc #3 <abc>
5 astore_2
6 return
三、String、StringBuilder和StringBuffer的区别
- String是字符串 常量,而 tringBuilder和StringBuffer都是字符串 变量
- StringBuilder是 线程不安全 的,而StringBuffer是 线程安全 的,这样就以为者后者会带来额外的系统开销,所以StringBuilder的效率比StringBuffer高
- String每次修改操作都要在堆内存中new一个对象,而StringBuffer和StringBuilder不用,并且提供了一定的缓存功能,默认16个字节数组的大小。扩容就原来的大小 x 2 + 2,可以考虑初始化StringBuilder的大小来提高代码的效率。
四、一些题目
1.下面程序运行的结果是什么
String s1 = "abc";
StringBuffer s2 = new StringBuffer(s1);
System.out.println(s1.equals(s2)); //false String的equals有对参数进行instance of String判断
StringBuffer s3 = new StringBuffer("abc");
System.out.println(s3.equals("abc")); //StringBuffer没有重写equals方法,实际上是 == 比较对象,StringBuilder也是
System.out.println(s3.toString().equals("abc")); //true 比较的是值
String s4 = "abc";
System.out.println("abc"==s4.subString(0)); //true,如果subString的index是0,直接返回对象
System.out.println("abc"==s4.subString(1)); //false,不为0就new一个sub之后的对象返回
除此之外,toLowerCase和toUpperCase都是new一个对象返回
2.下面语句一共创建了多少个对象
String str = new String("xyz");
这是一道有歧义的题,因为没有说明时机,实际上可以问涉及到几个,答案是两个,一个是在类加载过程中在常量池里面创建的"abc"对象,另外一个是运行期间创建在堆内存的"abc"对象。