1、String
1.1、基本特性
- String s = “hello”; String s = new String(“hello”)
- String类声明为final,不可继承
- 实现了Serializable接口,支持序列化;支持Comparable接口,可以比较大小
- JDK8及以前内部定义final char[] value存储字符串,JDK9及以后使用byte[]。根据编码方式不一样,存储时加上编码标记,UTF-8的使用两个byte存储,为了保持一致,StringBuffer和StringBuilder底层也采用byte存储
- 不可变性:
- 重新赋值要新建String对象并分配空间;
- 连接String要新建String对象并分配空间;
- 使用replace()方法要新建String对象并分配空间;
- StringBuffer:对象能多次修改,不产生新的未使用对象,线程安全;
- StringBuilder:对象能多次修改,不产生新的未使用对象,线程不安全,速度快;
1.2、字符串常量池
- 通过字面量赋值(String s = “hello”),此时字符串值声明在字符串常量池中
- 字符串常量池中不会存储相同内容字符串String s1 = “hello”; String s2 = “hello”;s1==s2是true
- String Pool是固定大小的HashTable,保存了字符串,类似于HashMap,可以根据字符串的hashcode找到对应entry。HashTable默认长度是1009,如果存的String过多,那么造成Hash冲突,存储的链表很长,当调用String.intern时性能下降(String.intern:如果一个String没有在Pool中,那么将其放入)
- 可以使用-XX:StringTableSize设置StringTable长度
- JDK6中StringTable长度固定为1009
- JDK7中默认60013
- JDK8开始,如果要设置StringTable长度,1009是最小值
2、String的内存分配
2.1、String两种使用方法
- 字面量:String s = “hello”
- 对象:String s = new String(“hello”),可以使用String.intern()方法,其实在new String(“hello”)中的“hello”其实也是字面量,会存储在字符串常量池中,同时堆中也会有一个对象,返回的便是堆中的对象
2.2、字符串常量池
- JDK6之前,字符串常量池放在永久代(方法区)
- JDK7后,字符串常量池放在Java堆中
- 字符串都存在堆中,和普通的对象一样,调优时考虑调整堆就行了
2.3、StringTable为什么要调整到堆中?
- 永久代的默认大小比较小
- 永久代垃圾回收频率低
3、字符串拼接操作
3.1、常量与常量拼接
- 结果还是在常量池中,在前端编译时就优化了
- 常量池中不会存储相同内容的常量
3.2、变量拼接
- 拼接中只要有一个是变量(final修饰的不是变量,是常量引用),那么结果就要存在堆中,变量拼接的原理是StringBuilder
- 变量拼接细节:
- 首先new一个StringBuilder对象s;
- 然后调用s.append(),拼接多少个变量调用多少次;
- 调用s.toString()转为String(相当于new String);
- 字符串拼接不一定都是用StringBuilder,拼接的是字符串常量或常量引用、字面量,直接在前端编译期就优化了
- 所以在代码中,尽量使用final修饰类、方法、基本数据类型、引用数据类型;
- 如果拼接后的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回地址
3.3、拼接效率对比
- String字符串拼接:s=”h”+”a”,实际实现中,会新建StringBulder和String对象,对象多了要GC,STW,效率低
- StringBuilder.append()方法拼接:自始至终只有一个StringBuilder对象
- StringBuilder改进:实际开发中,新建StringBuilder对象时,默认的底层存储byte数组长度是16,不够了再拓展,这样会耽误时间,那么可以通过s = new StringBuilder(highLevel),使用带参的构造器新建对象,这样直接指定byte数组长度
4、Intern()的使用
4.1、例子1
- New String(“ab”)
- new关键字在堆空间创建的对象“ab”
- 同样会在字符串常量池中有一个对象“ab”
- New String(“a”)+new String(“b”)
- new StringBuilder对象
- new String(“a”)
- 常量池中的”a”
- new String(“b”)
- 常量池中的”b”
- new String(“ab”),虽然在堆中有了这个对象,但是在常量池中并没有生成”ab”
4.2、例子2
- String s1 = new String(“1”);
- s1.intern();(此时该方法并不起作用,因为上一行代码已经将”1”存在字符串常量池中)
- String s2 = ”1”;
- s1 == s2 ;(为false,s1指向的是堆中的String对象,s2指向的是字符常量池中的对象)
- String s3 = new String(“1”) + new String(“1”);堆中有,池中无(此时s3指向的地址是堆中的new String(“11”),在字符串常量池中并不存在”11” )
-
s3.intern();(JDK6中会在常量池中生成”11”新对象;JDK7后只会在常量池中指向堆空间中的String对象,不会创建”11”新对象节约空间)
-
String s4 = “11”
-
s3==s4(JDK6中是false,s3指向堆中对象,s4指向字符串常量池;JDK7后是true,s3指向堆中对象,s4指向字符串常量池,但是池中又指向堆中,所以s3和s4都是指向堆空间的String对象)
4.3、String.intern()用法
- Jdk6中,尝试将字符串放入字符串常量池
- 如果池中已经有,那么不会放入了,返回串池中已有的地址
- 如果没有,那么将对象赋值一份,放入串池,并返回在串池中的地址
- Jdk7后,尝试将字符串放入字符串常量池
- 如果池中已经有,那么不会放入了,返回串池中已有的地址
- 如果没有,那么将对象的引用地址(堆中地址)复制一份,放入串池,并返回串池中的引用地址
- Intern()效率:对于程序中大量重复的字符串,尤其其中存在很多重复字符串时,使用intern()可以节省内存空间。
(第三张图中,intern()方法不执行了,所以在JDK8中也就不会将s2的引用转移到堆中对象)
4.4、例子(JDK8)
String s1 = new String("1")+new String("1");
s1.intern();
String s2 = "11";
System.out.println(s1==s2);//true
String s3 = new String("1");
s3.intern();
String s4 = "1";
System.out.println(s3==s4);//false
- s1属于拼接,所以在调用intern之前,常量池中没有11及其引用,等到调用intern之后,常量池中有了11的引用,指向堆中的对象,s1指向这个对象。
- s2在定义时,指向常量池中11的引用,其实也就是指向堆中的对象(和s1一样),所以输出true
- s3不属于拼接,在定义的时候,先在常量池中有一个1,然后在堆中有一个对象,s3指向这个对象。于是调用intern无效,因为池中已经1了
- s4定义为字面量,直接指向常量池中的1,而s3指向的是堆中的对象,二者不同,所以输出false
5、StringTable的垃圾回收
-
Int I = 0;
-
String.valueOf(i).intern();
-
上面代码中,String.value()会new String,调用intern,
-
当创建的new String足够多的时候,堆会进行GC,进行GC的时候,常量池中的字面量也被GC
6、G1中的String去重操作
6.1、背景
- 堆中存活的对象中,String占了25%
- 堆中的String,重复的有超过一半,总比例是13.5%
- String平均长度是45
- 所以堆中的String存在内存浪费
6.2、实现:(not impotant)
- 垃圾回收器工作时,会访问堆上存活的对象。对每一个对象都要检查是否属于候选的,要去重的String对象
- 如果是要去重的,那么将该对象插入到一个队列中,这个队列有一个去重线程专门管理。处理队列的一个元素,意味着从队列中删除这个元素,然后尝试去重它引用的String对象
- 使用HashTable记录所有被String对象使用的不重复的char数组(现在是byte数组),当去重时,检查HashTable,看堆中是否存在一样的char数组
- 如果存在,那么String对象调整去引用HashTable中已有的数据,释放原来引用的数组,最终被GC回收
- 如果查找失败,char数组被插入HashTable中,以后可以共享该数组
6.3、命令行实现:
- 使用UserStringDeduplication(bool)开启String去重,默认不开启,需要手动开启
- PrintStringDeduplicationStatistics(book)打印去重信息
- StringDeduplicationAgeThreshold(uintx)设置达到年龄的String要去重