String概述
- 声明为final,不可被继承
- 实现了Serializable和Comparable接口
- jdk8及之前用char数组实现,jdk9开始用byte数组实现(加上编码标记)
代表不可变的字符序列
字符串常量池,也称String Table,在Java堆中
- 固定大小的Hash Table,不能存储相同内容的字符串,完全相同的字符串字面量,应包含同样的Unicode字符序列,指向同一个String类实例
- -XX:StringTableSize设设置StringTable的长度
- 声明一个String变量,先使用equals判断常量池中是否有相等的字符串,如果没有则将这个String变量添加到常量池中,如果有则将返回一个地址给新声明的变量
- 在Java语言中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。
- 常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种。
- 直接使用双引号声明出来的string对象会直接存储在常量池中
- 比如:string info = “atgluigu.com”;
- 如果不是用双引号声明的string对象,可以使用String提供的intern()方法
- 直接使用双引号声明出来的string对象会直接存储在常量池中
- Java 6及以前,字符串常量池在方法区中,
Java7开始,字符串常量池的位置调整到Java堆中(新生区)
为什么分配在堆?
常量与常量的拼接结果放在常量池,原理是编译器优化
- 只要其中一个是变量,结果就在堆中,变量拼接的原理是StringBuilder
- 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串传对象放入池中,并返回对象地址 ```java String s1 = “java”; //常量池 String s2 = “python”; //常量池 String s3 = “javapython”; //常量池 String s4 = “java” + “python”; //常量池已存在 String s5 = “java” + s2; //堆 String s6 = s1 + “python”; //堆 String s7 = s1 + s2; //堆 String s8 = s6.intern(); //常量池已存在
System.out.println(s3 == s4); //true System.out.println(s4 == s5); //false System.out.println(s5 == s6); //false System.out.println(s4 == s7); //false System.out.println(s5 == s7); //false System.out.println(s3 == s8); //true
<a name="Bp5RQ"></a>
## 变量拼接的细节
s1 = "a"; s2 = "b"; 则 s1 + s2底层实现为:
1. StringBuilder s = new StringBuilder();
1. s.append("a")
1. s.append("b")
1. s.toString() --> 类似于 new String("ab")
- 如果引用的是常量或常量引用,则仍然使用编译期优化,那么不会在堆中创建新的对象,在准备变量的时候不是赋零值,而是直接赋常量值
```java
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
String s = s1 + s2;
System.out.println(s == s3); //true
执行效率
public class TestString4 {
public static void main(String[] args) {
long begin1 = System.currentTimeMillis();
String string = "nihao";
for (int i = 0; i < 100000; i++) {
string = string + i;
}
System.out.println(System.currentTimeMillis() - begin1);
System.out.println("=======================");
long begin2 = System.currentTimeMillis();
StringBuilder s = new StringBuilder();
for (int i = 0; i < 100000; i++) {
s.append(i);
}
System.out.println(System.currentTimeMillis() - begin2);
}
}
22840
=======================
3
通过StringBuilder代替String,改进:StringBuilder s = new StringBuilder(hihtLevel); 避免动态扩容
intern()
对于任何两个字符串s和t , s.intern() == t.intern()是true当且仅当s.equals(t)是true
- 如果不是用双引号声明的string对象,可以使用string提供的intern方法:方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。比如: string myInfo = new string (“I love atguigu” ) .intern() ;
- 也就是说,如果在任意字符串上调用string.intern方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式的值必定是true:
( “a” +”b” + “c” ) .intern ( ) == “abc” 通俗点讲,Interned string就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(string Intern Pool) 。
public class TestIntern {
public static void main(String[] args) {
String s1 = "yangtao";
String s2 = new String("yangtao").intern();
String s3 = new StringBuilder("yangtao").toString().intern();
System.out.println(s1 == s2);
System.out.println(s1 == s3);
}
}
以上s1、s2、s3都是指向字符串常量池中的一个地址,指向同一个字符串 “yangtao”
new String()
String str = new String(“ab”); 创建几个对象?
- 2个,分别为String对象(堆空间)、字符串常量池中的对象 “ab”
- String str = new String(“a”) + new String(“b”); 创建了几个对象
- 6个,StringBuilder、new String(“a”)、”a”、 new String(“b”)、 “b”、new String(“ab”)
题目
public class TestIntern2 {
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
}
}
结果:false、true
- false:行3创建了new String(“1”)、”1” 两个对象,因此调用intern()方法前,字符串常量池中就已经存在”1”了,而s是new String(“1”)的引用、s2是”1”的引用,一个指向对象,一个指向字符串常量池,因此结果为false
true:行8中创建的对象有5个,分别为StringBuilder、new String(“1”)、”1”、new String(“1”)、new String(“11”),在调用s3.intern()时不存在”11”这个变量,又因为jdk7开始,字符串常量池存储在堆中,因此为了节省空间,String s4 = “11” 并没有在字符串常量池中创建 “11”这个字符串,而是维护了一个引用,指向堆中 new String(“11”),因此s3 == s4结果为true
public class TestIntern3 {
public static void main(String[] args) {
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);
}
}
结果:false,s3指向堆中的一个对象、s4指向字符串常量池的”11”
总结
jdk6中,会将这个字符串对象尝试放入串池,如果串池中有,则不会放入,返回已有串池中的对象的地址;如果没有,会把对象复制一份,放入串池,返回串池中对象的地址
jdk7开始,将字符串对象尝试放入串池,如果串池中有,则不会放入,返回已有的串池中的对象的地址;如果没有,则会把对象的引用地址赋值一份,放入串池,并返回串池中的引用地址
效率测试
public class TestIntern6 {
static final int MAX_COUNT = 1000 * 10000;
static final String[] arr = new String[MAX_COUNT];
public static void main(String[] args) throws InterruptedException {
Integer[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
long start = System.currentTimeMillis();
for (int i = 0; i < MAX_COUNT; i++) {
// arr[i] = new String(String.valueOf(data[i % data.length])).intern();
arr[i] = new String(String.valueOf(data[i % data.length]));
}
long end = System.currentTimeMillis();
System.out.println("花费时间为:" + (end - start));
Thread.sleep(1000000);
}
}
如果需要存储大量重复的字符串,那么调用intern()方法,引用指向字符串常量而不是String对象,而String对象可以后期优化消除,因此,可以节约内存空间,加快字符串操作任务的执行速度。
StringTable的垃圾回收
运行参数:-XX:+PrintStringTableStatistics -XX:+PrintGCDetails
G1的String去重
背景
对许多Java应用(有大的也有小的)做的测试得出以下结果
- 堆存活数据集合里面String对象占了25%
- 堆存活数据集合里面重复的string对象有13.5%string对象的平均长度是45
许多大规模的Java应用的瓶颈在于内存,测试表明,在这些类型的应用里面,Java堆中存活的数据集合差不多25%是string对象。更进一步,这里面差不多一半string对象是重复的,重复的意思是说:string1.equals(string2)==true。堆上存在重复的string对象必然是一种内存的浪费。这个项目将在G1垃圾收集器中实现自动持续对重复的 String 对象进行去重,这样就能避免浪费内存。
实现
当垃圾收集器工作的时候,会访问堆上存活的对象。对每一个访问的对象都会检查是否是候选的要去重的String对象。
- 如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的String对象。
- 使用一个hashtable来记录所有的被String对象使用的不重复的char数组。当去重的时候,会查这个hashtable,来看堆上是否已经存在一个一模一样的char数组。
- 如果存在,string对象会被调整引用那个数组,释放对原来的数组的引用,最终会被垃圾收集器回收掉。
如果查找失败,char数组会被插入到hashtable,这样以后的时候就可以共享这个数组了。
命令
UseStringDeduplication (bool):开启string去重,默认是不开启的,需要手动开启。
- PrintstringDeduplicationstatistics (bool):打印详细的去重统计信息
- StringDeduplicationAgeThreshold (uintx):达到这个年龄的string对象被认为是去重的候选对象