19-String Table
2021-05-27 本文已影响0人
紫荆秋雪_文
一、String的基本特性
- String:使用一对""引起来表示
- String声明为 final 的,不可被继承
- String实现了Serializable接口:表示字符串是支持序列化的
- String实现了Comparable接口:表示字符串可以比较大小
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
- String的不可变性的体现
- 1、对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值
- 2、对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
- 3、调用String的replace()方法修改指定字符串或字符时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
- 通过字面量的方式给一个字符串赋值,此时的字符串值声明在字符串常量池中
- 字符串常量池中是不会存储相同内容的字符串的
- String的String Pool 是一个固定大小的HashTable,默认值大小长度为1009。如果放进String Pool的String太多,就会造成Hash冲突严重,从而导致链表很长,而链表长了后直接会造成的影响就是当调用String.intern()时性能会大幅下降
- 使用-XX:StringTableSize可设置StringTable的长度
- JDK6中StringTable是固定的,就是1009
- JDK7中StringTable的长度默认为60013
实例一:对字符串重新赋值
- 源码
public class StringTableTest1 {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "abc";
s1 = "def";
System.out.println(s1 == s2);
System.out.println(s1);
System.out.println(s2);
}
}
- 打印输出
false
def
abc
image.png
image.png
小结:s1和s2分别指向字符串常量池中不同的字符串,所以s1 != s2
实例二:对字符串进行连接操作
- 源码
public class StringTableTest2 {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "abc";
s1 += "def";
System.out.println(s1 == s2);
System.out.println(s1);
System.out.println(s2);
}
}
- 打印输出
false
abcdef
abc
- 内存布局 内存布局.png
- 字节码 image.png image.png image.png image.png
- 内存布局 image.png
小结
- 字节码1行和字节码3直接从字符串常量池中获取"abc"赋值给s1和s2
- 在处理 s1 += "def" 时,首先创建一个StringBuilder对象并且在初始化的时候从aload_1中拿到"abc",其次调用StringBuilder对象的append方法拼接"def"(字符串"def"也是从字符串常量池中获取),最后调用toString方法并且存储到astore_1中
实例三:调用String的replace()方法修改指定字符串或字符时
- 源码
public class StringTableTest3 {
public static void main(String[] args) {
String s1 = "abc";
String s2 = s1.replace('a', 'd');
System.out.println(s1 == s2);
System.out.println("dbc" == s2);
System.out.println(s1);
System.out.println(s2);
}
}
- 打印输出
false
false
abc
dbc
- 内存布局 image.png
-
字节码
image.png
image.png
image.png - 内存布局 image.png
二、String的内存分配
- 直接使用双引号声明出来的 String 对象会直接存储在常量池中
- 如果不是用双引号声明的String对象,可以使用String提供的 intern()方法存储到常量池中
- Java6及以前,字符串常量池存放在永久代
- Java7开始字符串常量池存储到Java堆中
- 调优应用时仅需要调整堆大小就可以了
- 推荐使用String.intern()
三、字符串拼接
1、常量与常量的拼接结果在常量池,编译期优化
public class StringTableTest5 {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "a" + "b" + "c";
System.out.println(s1 == s2);
System.out.println(s1);
System.out.println(s2);
}
}
image.png
2、常量池中不会存在相同内容的常量
3、在拼接过程中只要其中有一个是变量,结果就在堆中。变量拼接的原理是 StringBuilder
public class StringTableTest6 {
public static void main(String[] args) {
String s1 = "Hi-";
String s2 = "JavaEE";
String s3 = "Hi-JavaEE";
String s4 = "Hi-" + "JavaEE";
String s5 = s1 + "JavaEE";
String s6 = "Hi-" + s2;
String s7 = s1 + s2;
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
System.out.println(s3 == s7);
System.out.println(s5 == s6);
System.out.println(s5 == s7);
System.out.println(s6 == s7);
System.out.println(s6.equals(s7));
}
}
- 打印输出
true
false
false
false
false
false
false
true
image.png
- 解析字节码
0 ldc #2 <Hi-> // 从常量池取出 "Hi-"
2 astore_1 // 把 "Hi-"存储到 astore_1 —— s1
3 ldc #3 <JavaEE> // 从常量池取出 "JavaEE"
5 astore_2 // 把 "JavaEE"存储到 astore_2 —— s2
6 ldc #4 <Hi-JavaEE> // 从常量池取出 "Hi-JavaEE"
8 astore_3 // 把 "JavaEE"存储到 astore_3 —— s3
9 ldc #4 <Hi-JavaEE> // 由于是字面量拼接,所以直接从常量池中获取 "Hi-JavaEE"
11 astore 4 // 存储s4地址
13 new #5 <java/lang/StringBuilder> // 字符串拼接时有变量存在时,首先创建 StringBuilder
16 dup
17 invokespecial #6 <java/lang/StringBuilder.<init>> // StringBuilder.init初始化
20 aload_1 // s1 地址
21 invokevirtual #7 <java/lang/StringBuilder.append> // 拼接
24 ldc #3 <JavaEE> // 从常量池取出 "JavaEE"
26 invokevirtual #7 <java/lang/StringBuilder.append> // 拼接
29 invokevirtual #8 <java/lang/StringBuilder.toString> // StringBuilder 转 String
32 astore 5 // 存储s5
34 new #5 <java/lang/StringBuilder> // 创建 StringBuilder 对象
37 dup
38 invokespecial #6 <java/lang/StringBuilder.<init>> // StringBuilder.init 初始化
41 ldc #2 <Hi-> // 从常量池取出 "Hi-"
43 invokevirtual #7 <java/lang/StringBuilder.append> // 拼接
46 aload_2 // 从aload_2中获取数据,也就是 "JavaEE"
47 invokevirtual #7 <java/lang/StringBuilder.append> // 拼接
50 invokevirtual #8 <java/lang/StringBuilder.toString> // StringBuilder 转 String
53 astore 6 // 存储s6
55 new #5 <java/lang/StringBuilder> // 创建 StringBuilder 对象
58 dup
59 invokespecial #6 <java/lang/StringBuilder.<init>> // StringBuilder.init 初始化
62 aload_1 // 从aload_1中获取数据,也就是 "Hi-"
63 invokevirtual #7 <java/lang/StringBuilder.append> // 拼接
66 aload_2 // 从aload_2中获取数据,也就是 "JavaEE"
67 invokevirtual #7 <java/lang/StringBuilder.append> // 拼接
70 invokevirtual #8 <java/lang/StringBuilder.toString> // StringBuilder 转 String
73 astore 7 // 存储s7
75 getstatic #9 <java/lang/System.out>
- 字节码中使用StringBuilder对象拼接的对象s5、s6、s7都是新对象并且存储在堆中
- final
public class StringTableTest6 {
public static void main(String[] args) {
final String s1 = "Hi-";
final String s2 = "JavaEE";
String s3 = "Hi-JavaEE";
String s4 = "Hi-" + "JavaEE";
String s5 = s1 + "JavaEE";
String s6 = "Hi-" + s2;
String s7 = s1 + s2;
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
System.out.println(s3 == s7);
System.out.println(s5 == s6);
System.out.println(s5 == s7);
System.out.println(s6 == s7);
System.out.println(s6.equals(s7));
}
}
- 打印输出
true
true
true
true
true
true
true
true
- 字节码
0 ldc #2 <Hi->
2 astore_1
3 ldc #3 <JavaEE>
5 astore_2
6 ldc #4 <Hi-JavaEE>
8 astore_3
9 ldc #4 <Hi-JavaEE>
11 astore 4
13 ldc #4 <Hi-JavaEE>
15 astore 5
17 ldc #4 <Hi-JavaEE>
19 astore 6
21 ldc #4 <Hi-JavaEE>
23 astore 7
25 getstatic #5 <java/lang/System.out>
4、如果拼接的结果调用intern方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
5、StringBuilder的效率
public class StringTableTest7 {
public static void main(String[] args) {
// plusStr(100000); // 58380
stringBuilderPlusStr(100000); // 11
}
public static void plusStr(int count) {
String s1 = "Raven";
long start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
s1 += "_Raven_";
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
public static void stringBuilderPlusStr(int count) {
StringBuilder s1 = new StringBuilder("Raven");
long start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
s1.append("_Raven_");
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
- 当可预期长度时,可以指定StringBuilder(length)来提高效率,防止多次动态分配空间
四、intern()的使用
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
- 如果不是用双引号声明的String对象,可以使用String提供的intern()方法:intern()方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。也就是说,如果在任意字符串上调用String.intern方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。intern()方法确保了字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。
实例1
public class StringTableTest9 {
public static void main(String[] args) {
String s = new String("1");
String s1 = s.intern();
String s2 = "1";
System.out.println(s == s1);
System.out.println(s == s2);
System.out.println(s1 == s2);
}
}
- 打印输出
false
false
true
- 字节码
0 new #2 <java/lang/String> // 创建 String 对象s
3 dup
4 ldc #3 <1> // 从字符串常量池中获取 1
6 invokespecial #4 <java/lang/String.<init>> //String.init 初始化
9 astore_1 // 把 s 存储到 astore_1
10 aload_1 // 从 aload_1 获取数据
11 invokevirtual #5 <java/lang/String.intern> // 调用String.intern
14 astore_2 // 把 s1 存储到 astore_2
15 ldc #3 <1> // 从字符串常量池中获取 1
17 astore_3 // 把 s3 存储到 astore_3
18 getstatic #6 <java/lang/System.out>
分析
- 1、在创建s对象的时候,字符串常量池中已经存在"1"
- 2、s.intern的时候,由于字符串常量池中已经存在"1",所以s对象指向的是堆空间,s1对象指向的是字符串常量池
- 3、s2直接使用的是字面量,所以s2对象指向的是字符串常量池
- 4、所以s != s1、s != s2、s1 == s2
- 内存分布 内存分布.png
实例2
- 源码
public class StringTableTest10 {
public static void main(String[] args) {
String s = new String("1") + new String("1");
String s1 = s.intern();
String s2 = "11";
System.out.println(s == s1);
System.out.println(s == s2);
System.out.println(s1 == s2);
}
}
- 输出打印
true
true
true
- 字节码
0 new #2 <java/lang/StringBuilder> // 创建 StringBuilder 对象
3 dup
4 invokespecial #3 <java/lang/StringBuilder.<init>> // StringBuilder.init 初始化
7 new #4 <java/lang/String> // 创建 String 对象
10 dup
11 ldc #5 <1> // 从字符串常量池中获取 1
13 invokespecial #6 <java/lang/String.<init>> // String.init 初始化
16 invokevirtual #7 <java/lang/StringBuilder.append> // StringBuilder对象拼接字符串 1
19 new #4 <java/lang/String> // 创建 String 对象
22 dup
23 ldc #5 <1> // 从字符串常量池中获取 1
25 invokespecial #6 <java/lang/String.<init>> // String.init 初始化
28 invokevirtual #7 <java/lang/StringBuilder.append> // StringBuilder对象拼接字符串 1
31 invokevirtual #8 <java/lang/StringBuilder.toString> // StringBuilder对象 转 String
34 astore_1
35 aload_1
36 invokevirtual #9 <java/lang/String.intern>
分析
- 1、在创建 s 对象的时候,字符串常量池中已经存在 "1"
- 2、首页要创建一个StringBuilder对象用来拼接2个String对象
- 3、创建两个String对象(new String("1"))
- 4、StringBuilder.toString方法把拼接好的字符串"11",赋值给 s 对象,此时字符串常量池中是不存在 "11"
- 5、s.intern时会先看字符串常量池中是否存在"11"对象,如果不存在会把堆中的"11"地址引用到字符串常量池中(在jdk7之前,会拷贝堆中对象放到常量池中),并把字符串地址引用返回,到此s、s1 指向相同地址(字符串常量池)
- 6、s2对象直接使用字面量"11"赋值,由于此时字符串常量池中已有"11",所以s2也是指向字符串常量池
- 7、现在s、s1、s2都指向 字符串常量池 中
小结:String的intern()使用
-
JDK6中,将这个字符串对象尝试放入字符串常量池
- 如果字符串常量池中已有,则并不会放入。返回已有的字符串常量池中的对象的地址
- 如果没有,会把此对象复制一份,放入字符串常量池,并返回字符串常量池中的对象地址
-
JDK7开始,将这个字符串对象尝试放入字符串常量池
- 如果字符串常量池中已有,则并不会放入,返回已有的字符串常量池中的对象的地址
- 如果没有,则会把对象的引用地址复制一份,放入字符串常量池中,并返回字符串常量池中的引用地址
五、G1中String去重操作
背景:大多Java应用中
- 堆存活数据集合里面 String 对象占了 25%
- 堆中存活数据集合里面重复的 String 对象有 13.5%
- String 对象的平均长度是45
- 许多大规模的 Java 应用的瓶颈在于内存,测试表明,在这些类型的应用里面,Java堆中存活的数据集合差不多 25% 是String对象。这里面差不多一半 String 对象是重复的(string1.equals(string2)==true)。堆上存在重复的 String对象必然是一种内存的浪费。这个项目将在G1垃圾收集器中实现自动持续对重复的 String 对象进行去重,这样就能避免浪费内存
实现方式
- 1、当垃圾收集器工作的时候,会访问堆上存活的对象。对每一个访问的对象都会检查是否是候选的要去重的 String 对象
- 2、如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的 String 对象
- 3、使用一个 HashTable 来记录所有的被 String 对象使用的不重复的 char 数组。当去重的时候,会查这个 HashTable,来看堆上是否已经存在一个一模一样的char数组
- 4、如果存在,String 对象会被调整引用那个数组,释放对原来的数组的引用,最终会被垃圾收集器收掉
- 5、如果查找失败,char数组会被插入到 HashTable,这样以后的时候就可以共享这个数组了
命令行
- 开启 String 去重,默认不开启的,需要手动开启
UseStringDeduplication(bool);
- 打印详细的去重统计信息
PrintStringDeduplicationStatistics(bool);
- 达到这个年龄的 String 对象被认为是去重的后续对象
StringDeduplicationAgeThreshold(uintx)