写在最前,本篇文章大部分来源于b站尚硅谷JVM全套教程的提炼,并附带自己的理解。主要是为了帮助自己理解,和用于复习。如果同时还能对其他人有所裨益,那就更好不过了。如果有谬误的地方,还请不吝指出。
String基本特性
-
两种定义方式
- 使用字面量直接赋值
- new生成对象
-
声明为 final 不可被继承
-
JDK1.8及之前用char[]数组存储。JDK1.9之后使用byte[]数组容器存储。
为什么要改?
1.8及以前,用两个字节来表示一个字符,但在实际运用中,发现大多数字符串只使用了拉丁字符,它们只需要1个字节就能存放得下,这样来看,一半的空间就被浪费掉了。
所以改变了内部存储结构,从UTF-16到byte数组,并加上一个encoding-flag域,用于判断字符串该用哪种方式存储。
不仅如此,基于String类型的类都相应地进行了修改。 -
具备不可变的特性
基于字面量赋值的String变量存储于字符串常量池中。
一旦一个字符串常量被创建,就不会被修改。
譬如:+=、replace等方法只是新创建了一个字符串,并非在原来的字符串上修改。 -
字符串常量池不会存储相同内容的字符串。
String Pool 是一个固定大小的Hashtable,JDK6默认长度1009,JDK7以后默认60013。如果放进池的String非常多,就会冲突严重而使得链表变长,使得性能大幅下降。 长度是固定的,可以提前用-XX:StringTableSize
设置大小。在JDK6可以任意设置。JDK8及以后最小设置为1009。
String的内存分配
常量池类似一个Java系统级别提供的缓存。8种基本类型的常量池都是系统协调的。
String类型比较特殊,如果想要存放在常量池中:
如果字面量赋值,直接存在常量池中。
当我们调用intern方法时,会在常量池中生成对应字符串。(可以通过制造OOM证明常量池处于堆中)
为什么要将字符串常量池放在堆中?(JDK8以后)
- 永久代默认内存较小
- 永久代垃圾回收频率低
如果字符序列是一样的, 就不会创建新的字符串。
字符串拼接操作
- 常量与常量的拼接结果在常量池,原理是编译期优化(在生成字节码文件的时候就已经完成了拼接)
- 只要其中有一个变量,结果就在堆中(非常量池区域),原理是StringBuilder
- 如果拼接结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回对象地址。
代码示例
这里用我曾经使用过的一个代码例子:
String s1="hello";
String s2="hell";
String s3="o";
String s4="hello";
String s5=new String("hellow");
String s6=new String("hellow");
System.out.println(s1==s4); //1
System.out.println("hell"+"o"==s4);//2
System.out.println((s2+s3)==s4); //3
System.out.println(s5.intern()==s5);//4
第一个打印语句为true说明:字面量赋值,只要字符序列相同,就指向常量池中的同一个字符串。T
第二个打印语句为true说明:常量相加不会在堆中创建字符串**(在本文中,以堆指代常量池之外的部分)**
第三个打印语句为false说明:如果在拼接字符前后出现了变量,则相当于在堆空间中new String()(无论变量本身是否由字面量赋值)(其实就是StringBuilder::toString()方法)
intern():相当于做一个校验,判断字符串常量池中是否存在这个值,如果存在,则返回其地址;如果不存在,则在常量池中加载一份,再返回地址。所以第四个语句为false。
找到第一个打印语句的字节码指令
55 new #10 <java/lang/StringBuilder>
58 dup
59 invokespecial #11 <java/lang/StringBuilder.<init> : ()V>
62 aload_2
63 invokevirtual #12 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
66 aload_3
67 invokevirtual #12 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
70 invokevirtual #13 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
可以发现如果拼接字符串出现变量,其实是创建了一个StringBuilder来进行字符串拼接。
注意:如果变量被声明为final,即此变量中途不会再改变其引用对象,那么就会被视作为常量,不会使用StringBuilder进行拼接。当然,如果直接使用字面量拼接,也不会使用StringBuilder。
结合这些知识,我们来判断一下下面这两个方法哪个更快:
String method1(int level){
String src = "";
for(int i=0;i<level;i++){
src=src+"a";
}
return src;
}
String method2(int level){
StringBuilder src = new StringBuilder();
for(int i=0;i<level;i++){
src.append("a");
}
return src.toString();
}
实际上,在method1中的每次循环,都会创建一个新的StringBuilder对象,并且还要调用toString()方法,所以显然method2更快。
method2:内存中由于创建了较多的StringBuilder和String对象,内存占用更大,可能需要GC。
那么method1是否还有改进的空间的呢?
在创建StringBuilder对象时,有一个可选的参数capacity
,如果初始就能知道要容纳数组的大小,就可以避免扩容产生的额外的空间。
intern()的使用*
intern()方法官方解释 :对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。
如果不是通过双引号声明的Stirng对象,可以使用intern方法,从字符串常量池中查询当前字符串是否存在,若不存在,则将字符串放入池中,并返回池中的引用地址。
注意:调用intern方法的字符串无论是指向堆中的对象还是常量/字面量,都会去常量池中寻找字符串,并且只会返回常量池中相应字符串的地址。
intern()方法用于保证字符串在内存中只有一份拷贝。
intern的难题*
抛出intern问题之前,先问两个问题:
new String(“ab”)会创建几个对象?
假设之前常量池中不存在“ab”,则会创建两个对象。
堆中创建的对象和常量池中的对象。
new String(“a”)+new String(“b”)创建几个对象?
对象1:StringBuilder对象
对象2:new String(“a”)堆中对象
对象3:常量池中“a”对象
对象4:new String(“b”)堆中对象
对象5:常量池中“b”对象
对象6:StringBuilder对象 toString()方法创建对象于堆中(但没有在字符串常量池中生成对象)
下面来讲intern() (注意jdk6和jdk7/8的区别):
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s==s2);
String s3 = new String("1")+new String("1");
//注意此时常量池中没有创建新的对象
s3.intern();
String s4 = "11";
System.out.println(s3==s4);
jdk6中,结果是false, false
jdk7/8中,结果是false,true
jdk6,如果没有这个字符串,intern()一定会在常量池创建变量
是jdk7/8以后,如果已经在堆中创建了一个字符串对象且没放入常量池,则调用intern()会在常量池中存放一个指向这个对象的引用,使得后面所有字面量相同的字符串都会指向这一个对象,而不会再到常量池中创建新的变量。‘
总结几个关键点:
- intern()方法:注意jdk版本的差异,差异在于jdk7以后为了不重复创建字符串,会去堆中寻找。而jdk6只会在常量池中寻找。
- StringBuilder的toString()方法和new String的区别是,前者不会在常量池中创建对象
以下代码将第一行注释和不注释会得到一样的效果,证明它不会在常量池中创建对象。String s = new StringBuilder("a").append("b").toString(); String s1 = new String(new char[]{'a', 'b'}); s1.intern(); System.out.println("ab"==s1);//true
- 变量和常量的区别:无论是拼接字符串或是StringBuilder都要通过变量才能达到上述效果,因为如果是常量,会在编译期阶段就被优化为字面量了。
通过idea查看字节码文件,上一段代码其实就是:
我们更改代码:String s = "a" + "b"; String s1 =new String(new char[]{'a', 'b'}); s1.intern(); System.out.println("ab" == s1);
发现编译后依然是:String s = new StringBuilder("ab").toString();
证实了我们的想法。String s = "ab";
- 在大量使用字符串变量的场景,使用intern字符串可以降低内存消耗,因为堆中对象会被GC回收,而只剩下常量池中的对象。
G1的String去重操作
去重并不包括常量池,因为其本身就只有一份,而是指堆中new对象的去重。
通过测试发现(官方):
- 堆存活数据集合String对象占了25%
- 堆存活数据集合里,重复的String对象有13.5%
- String对象平均长度45
在G1垃圾回收器自动实现对重复的String对象去重,避免浪费内存。
步骤:
- 当垃圾回收器工作时,访问堆上存活的对象,检查是否是候选的要去重的String对象
- 如果是,则将其引用插入到队列中,一个去重线程在后台运行处理队列。处理一个元素意味着从队列里删除,并尝试去重它引用的String对象
- 使用一个hastable来记录所有的被String对象使用的不重复的char数组。当去重时,会查这个hashtable,看是否已经存在一个一样的char数组。
- 如果存在,则会对象被调整为引用那个数组,释放对原数组的引用,被垃圾回收器回收。
- 如果不存在,则char数组会被插入到hashtable,留给以后使用。
UseStringDeduplication
:开启string去重,默认不开启。