JVM系列(十一):String Table

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存储,为了保持一致,StringBufferStringBuilder底层也采用byte存储
  • 不可变性:
  1. 重新赋值要新建String对象并分配空间;
  2. 连接String要新建String对象并分配空间;
  3. 使用replace()方法要新建String对象并分配空间;
  4. StringBuffer:对象能多次修改,不产生新的未使用对象,线程安全;
  5. 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
  • 变量拼接细节
  1. 首先new一个StringBuilder对象s;
  2. 然后调用s.append(),拼接多少个变量调用多少次;
  3. 调用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”)
  1. new关键字在堆空间创建的对象“ab
  2. 同样会在字符串常量池中有一个对象“ab
  • New String(“a”)+new String(“b”)
  1. new StringBuilder对象
  2. new String(“a”)
  3. 常量池中的”a”
  4. new String(“b”)
  5. 常量池中的”b”
  6. 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==s4JDK6中是falses3指向堆中对象,s4指向字符串常量池JDK7后是trues3指向堆中对象,s4指向字符串常量池,但是池中又指向堆中,所以s3s4都是指向堆空间的String对象)

4.3、String.intern()用法

  • Jdk6中,尝试将字符串放入字符串常量池
  1. 如果池中已经有,那么不会放入了,返回串池中已有的地址
  2. 如果没有,那么将对象赋值一份,放入串池,并返回在串池中的地址
  • Jdk7后,尝试将字符串放入字符串常量池
  1. 如果池中已经有,那么不会放入了,返回串池中已有的地址
  2. 如果没有,那么将对象的引用地址(堆中地址)复制一份,放入串池,并返回串池中的引用地址
  • 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要去重
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值