第十三章:StringTable

String的不变性
String:字符串,使用一对双引号,引起来表示。
String s1 = “123”; //字面量方式定义,存储在字符串常量池中
String s2 = new String(“123”)
String类被声明为final的,不可被继承。

String实现了Serializable接口:表示字符串是支持序列化的。
实现了Comparable接口:表示String可以比较大小

String在JDK8及以前内部使用的是final char[] value数组。
在JDK9时更改为byte[]数组。

为什么要进行更改?——》为了减少空间浪费


String的基本特性
String代表不可变的字符序列,简称:不可变性。

 当对字符串重新赋值时,需要重新指定另外内存区域赋值,不能使用原有的value进行赋值。
 当对原有的字符串进行连接操作时,也需要重新指定另外内存区域赋值,不能使用原有的value进行赋值。
 当调用String的replace()方法修改指定字符或字符串时,也需要重新指定另外内存区域赋值,不能使用原有的value进行赋值。
通常字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。

String底层——HashTable结构
字符串常量池中不会存储相同内容的字符串。

String的String Pool是一个固定大小的HashTable,具有默认值大小。当放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了以后直接会造成的影响就是当调用String.intern时性能大幅下降。
使用-XX:StringTableSize可设置StringTable的长度。
在JDK6中StringTable是固定的,就是1009的长度。
在JDK7中,StringTable的长度默认值是60013

从JDK8开始,1009是可设置的最小值。

String的内存分配

常量池就类似一个Java系统级别的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊,它的主要使用方法有两种。
①直接使用双引号声明出来的String对象会直接存储在常量池中。
②如果不是直接使用双引号声明的String对象,可以使用String提供的intern()方法
Java 6及以前,字符串常量池存放在永久代。
Java 7及以后,将其位置调整到了Java堆内。

为什么StringTable要从永久代改变到堆中?
① 永久代PermSize空间较小
② 永久代垃圾回收频率低
在开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足,放到堆里后,能够及时的被回收。

String的基本操作
Java语言规范里要求完全相同的字符串字面量,应该包含同样的Unicode字符序列(包含同一份码点序列的常量),并且必须是指向同一个String类实例。

执行上述代码发现,在第二次输出1到10字符串的时候,该线程中字符串常量池中的数量不再增加。

字符串拼接操作
① 常量与常量的拼接结果在常量池,原理是编译器优化
② 常量池中不会存在相同内容的常量
③ 只要其中有一个是变量,结果就在堆(堆中非String常量池)中。变量拼接的原理是StringBuilder,相当于在堆空间中new String()
④ 如果拼接的结果调用了intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。

Case 1:(条件1)常量与常量拼接

反编译后,发现代码直接优化成String s1 = “abc”;

Case 2:(条件3)其中一个拼接的是变量

Intern()方法:判断字符串常量池中是否存在某个值,如果存在,则返回常量池中该值的地址,如果常量池中不存在该值,则在常量池中创建,并个数加1。

拼接操作的底层原理

内部使用的StringBuiler+append操作。

使用final修饰:
字符串拼接操作使用的不一定是StringBuilder
如果拼接符号左右两侧都是字符串常量或者常量引用,则仍然使用编译器优化,即使用非StringBuilder的形式。

可以看到执行astore_3和astore_4操作时,使用的都是同一个常量池中的对象。

String拼接操作与StringBuffer的append操作效率对比
通过StringBuilder的append()的方式添加字符串的效率要远高于使用String的字符串拼接方式。

① StringBuilder的append()方式,自始至终只创建过一个StringBuilder对象
使用String的字符串拼接方式,创建过多个StringBuilder和String对象
② 使用String的字符串拼接方式,内存中由于创建了较多的StringBuilder和String对象,内存占用更大。如果进行GC的话,会占用更多的时间。

使用StringBuilder进一步优化:
在实际开发中,如果基本确定使用中持续添加的字符串长度不高于某个限定值的情况下,建议使用构造函数:
StringBuilder s = new StringBuilder(限定值);

Intern()的使用
返回一个字符串对象的规范表示。
当调用intern()方法时,如果在字符串常量池里有与指定的字符串内容相等的,就返回该对象;否则将该字符串对象加入到常量池中,并返回该对象的引用。

如何保证变量s指向的是字符串常量池中的数据呢?
① 使用字面量定义:String s = “test”;
② 调用intern()方法:
String s = new String(“test”).intern();
String s = new StringBuilder(“test”).toString().intern();

new String()创建了几个对象
面试题:new String(“ab”)创建了几个对象?

从字节码来看,是两个:

  1. 一个String对象在堆中
  2. 一个常量”ab”在常量池中(指令:ldc)。

问题拓展:new String(“a”) + new String(“b”)呢?

① 对象1:指令0——new StringBuilder()对象
② 对象2:指令4——new String(“a”)对象
③ 对象3:指令5——常量池中的对象“a”
④ 对象4:指令19——new String(“b”)对象
⑤ 对象5:指令23——常量池中的对象“b”
⑥ 对象6:指令31:StringBuilder对象执行toString()方法,该方法内部执行new String(),创建了一个String 对象
注意:在toString方法中,执行new String()方法,没有在常量池中创建字符串对象。

toString()的源码和字节码:

字节码中没有执行ldc指令,也就是没有把字符串对象存储到常量池中。

Intern()的面试难题
判断分别在jdk6和jdk7/8下,的输出结果

分析:
(1)先看第一部分:
① 执行String s = new String(“1”)产生了两个对象,一个是堆中的String对象,另一个是在字符串常量池中的内容等于“1”的对象。S是常量池中”1”的引用。
② 执行s.intern()没有产生任何结果。
③ 再执行String s2 = “1”,直接让s2指向常量池中的”1”对象。因此s和s2指向的不是同一个地址。
(2)再看第二部分
① 先执行String s3 = new String(“1”) + new String(“1”)最终没有在字符串常量池中创建”11”对象。S3指向堆中的一个String对象,内容为”11”。
② 再执行s3.intern(),因为之前常量池中没有”11”对象,因此在常量池中创建”11”对象。但是这里有个坑,因为在堆中已经有了现成的”11”对象了,所以常量池中的该对象就直接是s3的地址,相当于对s3的引用。
③ 执行String s4 = “11”在常量池中找到了对s3的引用
④ 因此最终s3和s4其实指向的是同一个地址。
Jdk6 v.s. jdk7/8

面试题拓展

总结String的intern()方法:
① Jdk1.6中,将这个字符串对象尝试放入常量池
a) 如果常量池中有,则不会放入,并且返回已有的池中的对象地址
b) 如果没有,会把该对象复制一份,放入池中,并返回池中的对象地址
② Jdk1.7起,将这个字符串对象尝试放入常量池
a) 如果常量池中有,则不会放入,并且返回已有的池中的对象地址
b) 如果没有,会把该对象的引用地址复制一份,放入池中,并返回常量池中的引用地址

Intern()练习1
Case 1:

posted @ 2020-07-02 16:04  scnb  阅读(155)  评论(0)    收藏  举报