文章目录
1 关于String
1.1 String的基本特性
1,String类型的值即字符串
String str = "str";
String str2 = new String("str2");
2,String类是final的,不可被继承
3,String实现了Seriallizable接口,表示字符串支持序列化
String实现了Comparable接口,表示字符串可以比较大小
4,String在JDK8及之前内部字符串数据存储的结构为 final char[]
jdk9String类存储数据的结构改为了byte[]
这样调整的原因是一个char是两个byte
而开发中使用数字或英语字符较多,一个byte就能存下,使用char浪费了一半的byte
对于中文等存储时根据字符编码使用两个byte来存储
基于String的StringBuffer,StringBuilder等都做成了更新
String是不可变的字符序列,对任何字符串的修改/替换/拼接都不会影响原先的字符串,而是生成新的字符串:
String s1 = "abc";
String s2 = "abc";
sout(s1 == s2);
// 这里的结果是true 因为s1和s2所指向的"abc"是同一个
// 因为字面量赋值的字符串存储在字符串常量池,字符串常量池不允许出现相同的字符串常量
s2 += "def";
sout(s1 == s2);
// 这里的结果是false 当对字符串s2进行修改时,原来的"abc"不会改变
// 会在字符串常量池中生成新的字符串"abcdef"
sout(s1);
sout(s2);
// s1仍然是"abc" s2此时是"abcdef"
// 此时字符串常量池有两个常量 "abc" 和"abcdef"
// s1指向"abc" s2指向"abcdef"
通过字面量的方式(区别于new String(“xx”))给一个字符串赋值,此时的字符串值声明在字符串常量池中,字符串常量池中不会存储相同的字符串
1.1.1 字符串常量池的结构
字符串常量池是不会存储相同内容的字符串的,因为字符串常量池底层是一个固定大小的HashTable(数组+链表),如果放进字符串常量池的String非常多,就会造成Hash冲突,从而导致链表过长,而链表太长的后果就是当调用
String.intern()(如果字符串常量池中没有String,则在字符串常量池中生成)时性能会大幅下降
可以使用
-XX:StringTableSize设置字符串常量池HashTable的长度
在JDK6中字符串常量池中的HashTabel的长度默认是1009
所以如果有大量的字符串存入字符串常量池就会导致效率下降
在JDK7中,字符串常量池中的HashTable默认为60013
在JDK8及以后,默认为60013,1009是可以设置的最小值
1.1.2 new String和直接赋值
除了使用String str = “xxx”;外还可以使用String str = new String(“xxx”)的形式创建字符串,这两者的机制是不同的
String str = "xxx";
采用字面量直接赋值的方式,str被存储在堆中的字符串常量池
String str = new String("xxx");
采用新建对象的方式,会执行两个步骤
1,查看字符串常量池中有无字符串"xxx"
2,如果有,在堆中Eden中创建"xxx"的拷贝对象
3,如果没有,在字符串常量池中创建"xxx"
再在堆中Eden中创建一个字符串常量池中"xxx"的拷贝对象
示例:
上述代码通过debug16行之前一共有2225个String对象
但执行了String str3 = new String(“def”);后一共有2227个对象
也就是说String str3 = new String(“def”);创建了两个对象,先检查字符串常量池有无"def",发现没有则在字符串常量池创建一个"def",再在堆中Eden区域创建一个"def",这两个字符串对象值相同,地址不同
String str = "xx";
只在字符串常量池创建了一个字符串对象
String str = new String("xx");
可能创建了一个,也可能创建两个字符串对象
取决于运行本行代码之前字符串常量池中有无"xx"
扩展:
new String("a") + new String("b")创建了几个对象?
由于带变量的字符串拼接采用StringBuilder的原理
所以先创建一个StringBuilder对象
接着通过new 创建了两个"a"
通过StringBuilder的append方法将a拼接
接着通过new 创建了两个"b"
通过StringBuilder的append方法将b拼接
接着通过StringBuilder的toString方法在堆中生成一个"ab"
(StringBuilder的toString方法虽然会使用new String()
但不会向字符串常量池中生成字符串)
最多创建6个对象 最少创建4个对象
1.2 StringTable的使用和位置
在Java中有8大基本类型,和特殊的String类型,这些类型为了让它们运行过程中更快,更节省内存,都提供了一种常量池的概念,常量池就是一个类似于Java系统级别提供的缓存,8大基本类型的常量池都是系统调节的,而String类型的常量池比较特殊,它的使用方式有两种:
1,直接使用双引号声明的String对象会存储在堆中字符串常量池
如String str = "str";
2,如果不是用双引号声明的String对象,可以使用String提供的intern()方法
在JDK6时字符串常量池在永久代,7及以后,字符串常量池被存放在堆中,因为字符串也是对象,需要被回收,而永久代容量较小,存放大量的字符串会OOM,且永久代的GC频率较低,对无效字符串的回收效率很低,就更容易导致永久代OOM
由new String(“xx”)产生的字符串对象可以看作普通对象,创建后先存储在堆中的Eden区域
1.3 String的基本操作
1.3.1 字符串常量池中存储的字符串唯一
通过字面量声明的字符串会存储在字符串常量池中,下面的程序,从11-16行声明字符串并打印,通过debug显示每次会新生成一个字符串,但从18-23行每次打印时,会发现字符串对象不再增加
Java规范要求相同的字符串字面量,应该包含相同的Unicode字符序列,且指向同一个String类实例
1.3.2 字符串常量池存储在堆中
执行toString()时,将生成的字符串以字面量的形式存储到了堆中的字符串常量池,将该字符串的引用保存到了局部变量表
Heap内存示意:
1.4 字符串拼接操作
1,字符串常量和字符串常量的拼接结果存储在字符串常量池
原理是编译期优化
2,字符串常量池中不会存储相同的字符串
3,拼接过程中只要有变量,变量拼接的原理是StringBuilder
先生成一个StringBuilder对象 调用append方法拼接对象
将最终的拼接结果调用toString()得到新的字符串直接存储到堆中
4,如果拼接结果调用了interen()方法,且字符串常量池没有该结果
则将拼接的结果存储到字符串常量池,并返回其地址
如果字符串常量池存储了拼接结果,直接返回其地址
字面量的拼接过程:
带有变量的拼接过程:
1.4.1 带变量的字符串拼接的底层原理
带有变量的字符串拼接是基于StringBuilder的,如下示例:
查看上述方法执行的字节码:
0 ldc #2 <a>
2 astore_1
// 从常量池中取出a 存放在局部变量表的slot1
3 ldc #3 <b>
5 astore_2
// 从常量池中取出b 存放在局部变量表的slot2
6 ldc #4 <ab>
8 astore_3
// 从常量池中取出ab 存放在局部变量表的slot3
9 new #5 <java/lang/StringBuilder>
// new一个StringBuilder对象
12 dup
13 invokespecial #6 <java/lang/StringBuilder.<init>>
// 执行StringBuilder的构造方法
16 aload_1
17 invokevirtual #7 <java/lang/StringBuilder.append>
// 从局部变量表的slot1取出值 即a 调用StringBuilder对象的append方法
20 aload_2
21 invokevirtual #7 <java/lang/StringBuilder.append>
// 从局部变量表的slot2取出值 即b 调用StringBuilder对象的append方法
24 invokevirtual #8 <java/lang/StringBuilder.toString>
27 astore 4
// 调用StringBuilder对象的toString方法 生成一个新字符串存储到堆中
// 将字符串对象的引用存储在局部变量表的slot4
29 getstatic #9 <java/lang/System.out>
32 aload_3
33 aload 4
35 if_acmpne 42 (+7)
38 iconst_1
39 goto 43 (+4)
42 iconst_0
43 invokevirtual #10 <java/io/PrintStream.println>
46 return
通过debug看出变量拼接后只生成了一个新的String对象:
如果拼接符号左右两边都是字符串字面量,那么就会使用编译期优化
1.4.2 直接拼接与append的效率比较
使用字符串直接拼接:
使用一个StringBuilder对象的append方法:
使用一个StringBuffer对象的append方法:
效率分析:
虽然带变量的直接拼接底层也是通过StringBuilder的append()来完成的
但是10000次拼接就生成了10000个StringBuilder对象
每个StringBuilder还需要调用toString()来生成一个字符串存储到堆里
因此又生成了10000个String对象
生成10000个StringBuilder对象和10000个String对象的过程也需要时间,且浪费了大量堆空间
并且每次循环前的StringBuilder和String对象失去了引用
需要被回收 频繁的GC又会导致多次STW 导致整个用户线程执行速度降低
而使用1个StringBuilder或StringBuffer完成拼接能节约数倍的空间和时间
优化建议:
面对大量的字符串拼接,创建一个StringBuilder多次append即可
并且StringBuilder对象初始是16byte数组,每当容量不够时,需要不断进行扩容
扩容也需要时间,为此还可以创建StringBuilder对象时,使用带参构造
参数就是StringBuilder对象的数组长度,设置合理时,就省去了扩容的时间
StringBuilder使用无参构造时,底层的数组需要不断扩容:
StringBuilder使用带参构造时,参数合理时不需要扩容可以更快:
1.5 intern()的使用
对于非字面量声明的String对象,可以使用String类提供的intern()方法,intern()会从字符串常量池中查询当前字符串是否存在,若不存在则将当前字符串放入字符串常量池,并返回其在字符串常量池的地址,如果存在,则直接返回字符串常量池中对应字符串的地址
String str = new String("some message").intern();
intern()方法确保字符串常量池中存在一份拷贝
这样可以节约内存空间,加快字符串操作的执行速度
在JDK6及之前 intern()方法先检查字符串常量池有无对应字符串对象
无则在字符串常量池中创建新对象,并返回新对象的地址
有则返回字符串常量池中已存在对象的地址
在JDK7及之后 intern()方法先检查字符串常量池有无对应字符串对象
无则将该对象的地址保存在字符串常量池,并返回该地址
有则返回字符串常量池中已存在对象的地址
1.5.1 关于intern()的练习
练习1:
1 String s = new String("1");
2 s.intern();
3 String s2 = "1";
4 System.out.println(s == s2); // false
练习2:
1 String s3 = new String("1") + new String("1");
2 s3.intern();
3 String s4 = "11";
4 System.out.println(s3 == s4); // jdk6及之前是false
// jdk7之后是true
1.5.2 intern()的空间效率测试
不使用intern()时的内存情况:
上述程序,不使用intern()重复创建字符串对象,在堆中保留了约一百万个字符串对象
使用intern()时的内存情况:
使用intern()重复创建字符串时,在堆中只保留约一万个字符串对象,它也在堆中创建了大量的字符串对象,只是后续不再使用堆中的字符串对象,导致这些对象没有引用被GC
对于程序中需要大量创建字符串的地方
并且有很多重复的字符串时
使用intern()可以极大的节省堆空间内存
for(int i = 0; i < MAX_COUNT; i++) {
arr[i] = String.valueOf(i % data.length).intern();
}
其原理是使用的都是字符串常量池中同一对象的地址
堆中被创建的字符串对象没有引用关系被GC
StringTable(字符串常量池)也会发生GC
1.6 G1对堆中String的去重
对于很多Java应用做出测试得到一个结果:
1,堆中存活的数据String占了25%
2,堆中存活的重复的String对象有13.5%
3,String对象的平均长度是45B
许多大规模Java应用的瓶颈在于内存,测试表明这些类型的应用中,堆中存在大量的重复字符串对象,即str1.equals(str2) = true,堆中重复的String对象是一种内存的浪费,为此G1垃圾收集器提供了对堆中重复的String对象去重的功能,以减少内存浪费,提高程序性能
HotSpot VM对堆中重复String去重的操作默认是关闭的,需要手动打开:
-XX:+UseStringDeduplication
只适用于G1