13.1 String的基本特性
- String声明为final的,不可被继承
- String实现了Serializable接口:表示字符串是支持序列化的;实现了Comparable接口:表示String可以比较大小
- String在jdk8以前内部定义了final char[] value用于存储字符串数据,jdk9改为byte[]
- String:代表不可变的字符序列。不可变性
public class StringTest1 {
public static void test1() {
// 字面量定义的方式,“abc”存储在字符串常量池中
String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2);//判断地址,true
s1 = "hello";
System.out.println(s1 == s2);//false
System.out.println(s1);
System.out.println(s2);
System.out.println("----------------");
}
public static void test2() {
String s1 = "abc";
String s2 = "abc";
// 只要进行了修改,就会重新创建一个对象,这就是不可变性
s2 += "def";
System.out.println(s1);//abc
System.out.println(s2);//abcdef
System.out.println("----------------");
}
public static void test3() {
String s1 = "abc";
String s2 = s1.replace('a', 'm');
System.out.println(s1);//abc
System.out.println(s2);//mbc
}
public static void main(String[] args) {
test1();
test2();
test3();
}
}
面试题
public class StringExer {
String str = new String("good");
char [] ch = {'t','e','s','t'};
public void change(String str, char ch []) {
str = "test ok";
ch[0] = 'b';
}
public static void main(String[] args) {
StringExer ex = new StringExer();
ex.change(ex.str, ex.ch);
System.out.println(ex.str);//good
System.out.println(ex.ch);//best
}
}
字符串常量池是不会存储相同内容的字符串的
- String的String Pool是一个固定大小的HashTable,默认值大小长度是1009。如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表很长,而链表长了会导致调用string.intern性能大幅下降
- 使用-XX:SttingTableSize设置StringTable长度
- jdk6中,长度是1009,在jdk7中默认是60013,jdk8后,要求最小值1009
13.2 String的内存分配
- 在Java语言中有8中基本数据类型和一种比较特殊的类型String。这些类型为了使他们在运行时速度更快、更节省内存,都提供了一种常量池的概念
- 常量池类似于一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊,主要使用方法有2种
- 直接使用双引号声明出来的string对象会直接存储在常量池种。String info=“xxxx”
- 不是双引号声明的string对象,可以使用string提供的intern()方法。
String的内存分配
- JDK6以前,字符串常量池存放在永久代
- JDK中,将字符串常量池的位置调整到Java堆内
- JDK8元空间,字符串常量在堆
StringTable为什么要调整
1.permSize永久代大小默认比较小
2.永久代垃圾回收频率低
13.3 String的基本操作
Java语言规范里要求完全相同的字符串字面量,应该包含同样的Unicode字符序列(包含同一份码点序列的常量),并且必须是指向同一个String类实例
13.4 字符串拼接操作
- 常量与常量的拼接结果在常量池,原理是编译器优化
- 常量池中不会存在相同内容的常量
- 只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
- 如果拼接的结果调用inern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
public static void test1() {
String s1 = "a" + "b" + "c"; // 等同于 “abc”
String s2 = "abc"; // abc一定是存在常量池中,直接将常量池的地址返回
/**
* 最终java编译成.class,再执行.class
*/
System.out.println(s1 == s2); // true,因为存放在字符串常量池
System.out.println(s1.equals(s2)); // true
}
public static void test2() {
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop"; //编译器优化
//如果拼接符号的前后出现了变量,则相当于在堆空间中new String(),具体内容为拼接的结果
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
System.out.println(s3 == s4); // true
System.out.println(s3 == s5); // false
System.out.println(s3 == s6); // false
System.out.println(s3 == s7); // false
System.out.println(s5 == s6); // false
System.out.println(s5 == s7); // false
System.out.println(s6 == s7); // false
//intern()方法,判断字符串常量池中是否存在javaEEhadoop值,如果存在,则返回常量池中javaEEhadoop地址;如果不存在,则在常量池中加载一份javaEEhadoop,并返回此对象的地址
String s8 = s6.intern();
System.out.println(s3 == s8); // true
}
底层原理
拼接操作的底层其实是用的StringBuilder
s1+s2的执行细节:
StringBuilder s = new StringBuiler();
s.append(“a”);
s.append(“b”);
s.toString();---->约等于 new String(“ab”)
在JDK5后,使用的是StringBuiler。之前是用的StringBuffer
如果拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译器优化,即非StringBuiler的方式
针对final修饰类、方法、基本数据类型、引用数据类型的量的结构时,能使用final的时候建议加上
拼接操作和append性能对比
/*
* 通过StringBuilder的append()方式添加字符串的效率要远高于使用String字符串拼接方式。
* 自始至终只创建了一个StringBuilder对象,由于创建了较多对象,内存占用更大,如果进行GC,花费更多时间
* 在实际开发中,要前后添加的字符串长度不高于某个值,使用构造器StringBuilder(int capacity)
*/
public static void method1(int highLevel) {//耗时4000ms
String src = "";
for (int i = 0; i < highLevel; i++) {
src += "a"; // 每次循环都会创建一个StringBuilder对象
}
}
public static void method2(int highLevel) {//耗时7ms
StringBuilder sb = new StringBuilder();
for (int i = 0; i < highLevel; i++) {
sb.append("a");
}
}
13.5 intern()的使用
intern是一个native方法,调用底层的C方法
- 如果不是用双引号声明的String对象,可以使用String提供的intern方法,intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中
- 如果在任意字符串上调用String.intern方法,其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同
面试题
new String(“ab”)会创建几个对象?
new String(“a”)+new String(“b”)呢?
public class StringNewTest {
public static void main(String[] args) {
//一个对象是new关键字在堆空间创建的
//另一个对象是字符串常量池中的对象,字节码指令ldc
String str = new String("ab");
//对象1,new StringBuiler()
//对象2,new String("a")
//对象3,常量池中的”a“
//对象4,new String("b")
//对象5,常量池中的”b“
//对象6,StringBuiler的toString()方法,常量池中没有ab,new String("ab")
String str2 = new String("a") + new String("b");
}
}
str字节码指令
0 new #2 <java/lang/String>
3 dup
4 ldc #3 <ab>
6 invokespecial #4 <java/lang/String.<init>>
9 astore_1
10 return
str2字节码指令
0 new #2 <java/lang/StringBuilder>
3 dup
4 invokespecial #3 <java/lang/StringBuilder.<init>>
7 new #4 <java/lang/String>
10 dup
11 ldc #5 <a>
13 invokespecial #6 <java/lang/String.<init>>
16 invokevirtual #7 <java/lang/StringBuilder.append>
19 new #4 <java/lang/String>
22 dup
23 ldc #8 <b>
25 invokespecial #6 <java/lang/String.<init>>
28 invokevirtual #7 <java/lang/StringBuilder.append>
31 invokevirtual #9 <java/lang/StringBuilder.toString>
34 astore_1
35 return
intern()的使用,JDK6 VS JDK7/8
String s = new String("1");
s.intern(); //调用此方法前,字符串常量池已经存在了”1“
String s2 = "1";
System.out.println(s == s2); // JDK6:false JDK7/8:false
String s3 = new String("1") + new String("1");//s3的地址为new String("11")
//执行完上一行代码后,字符串常量池中,不存在”11“
s3.intern();//在字符串常量池中生成”11“,JDK6中创建一个新对象”11“,JDK7中常量池没有创建新对象,而是指向new的地址
String s4 = "11";//使用的是上一行代码执行时,在常量池生成的”11“的地址
System.out.println(s3 == s4); // JDK6:false JDK7/8:true
总结String的intern()的总结
JDK6中,将这个字符串对象尝试放入串池
如果串池中有,则并不会放入。返回已有的串池中的对象的地址
如果没有,会把此对象复制new一份,放入串池,并返回串池中的对象地址
JDK7起,将这个字符串对象尝试放入串池
如果串池中有,则并不会放入,返回已有的串池中对象的地址
如果没有,会把对象的引用地址复制,放入串池,并返回串池中的引用地址
空间效率测试
public class StringIntern2 {
static final int MAX_COUNT = 1000 * 10000;
static final String[] arr = new String[MAX_COUNT];
public static void main(String[] args) {
Integer [] data = new Integer[]{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();
}
long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start));
try {
Thread.sleep(1000000);
} catch (Exception e) {
e.getStackTrace();
}
}
}
结论:对于程序中大量存在的字符串,尤其存在很多重复字符串时,使用intern()方法能够节省内存空间
13.6 StiringTable的垃圾回收
public class StringGCTest {
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
String.valueOf(i).intern();
}
}
}
13.7 G1中的String去重操作
这里的去重,指的是堆的去重,而不是常量池中的,因为常量池本身就不会重复
背景
- 堆存活数据集里面String对象占了25%
- 堆存活数据里里重复的String对象有13.5%
- String对象的平均长度是45
- 堆上存在重复的String对象是一种内存浪费。在G1垃圾收集器实现自动持续对重复String对象进行去重
实现
- 当垃圾收集器工作时,会访问堆上存活的对象。对每一个访问的对象都检查是否时要去重的String对象
- 如果时,把这对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重他引用的String对象
- 使用一个hashtable记录所有被String对象使用的不重复的char数组。当去重的时候,会查这个hashtable,来看堆上是否已经存在一个一模一样的char数组
- 如果存在,String对象会被调整引用那个数组,释放堆原来数组的引用,最终被垃圾收集器回收
- 如果查找失败,char数组会插入到hashtable,以后就可以共享这个数组了
命令行
- UseStringDeduplication (bool):开启String去重,默认不开启
- PrintStringDeduplicationStatistics (bool):打印详细的去重统计信息
- StringDeduplicationAgeThreshold (uintx):达到这个年龄的String对象被认为是去重的候选对象