文章目录
1、String的基本特性
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
- String字符串,使用一对 “” 引起来表示
- 声明为final,不可
被继承
- 实现了
Serializable
接口:表示字符串是支持序列化的。实现了Comparable
接口,表示String可以比较大小. - String在
jdk8
及以前内部定义了final char[] value
用于存储字符串数据,jdk9
时改为byte[]
加上编码标记,节约了一些空间- 这样调整的原因是一个char是两个byte,而开发中使用数字或英语字符较多,一个byte就能存下,使用char浪费了一半的byte
- 对于中文等存储时根据字符编码使用两个byte来存储 基于String的StringBuffer,StringBuilder等都做成了更新
- String :代表
不可变的字符序列
。简称:不可变性- 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值
- 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
- 当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
- 通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。
1.1 字符串常量池
字符串常量池是不会存储相同内容的字符串的,因为字符串常量池底层是一个固定大小的HashTable(数组+链表),如果放进字符串常量池的String非常多,就会造成Hash冲突,从而导致链表过长,而链表太长的后果就是当调用。
String.intern()(如果字符串常量池中没有String,则在字符串常量池中生成)时性能会大幅下降
- 可以使用
-XX:StringTableSize
设置字符串常量池HashTable的长度 - 在
JDK6
中字符串常量池中的HashTabel的长度默认是1009
,所以如果有大量的字符串存入字符串常量池就会导致效率下降 - 在
JDK7
中,字符串常量池中的HashTable默认为60013
- 在
JDK8
及以后,默认为60013
,1009
是可以设置的最小值
1.2 new String和直接赋值
除了使用String str = “abc”;外还可以使用String str = new String(“abc”)的形式创建字符串,这两者的机制是不同的
String str = "xxx";
采用字面量直接赋值的方式,Str存储在字符串常量池
String str = new String("XXX");
new String对象会发生两种情况:
1.查看字符串常量池中是否有无字符串“xxx”
2.如果有,在堆中Eden区创建”XXX“的拷贝对象
3.如果没有,先将字符串”xxx“放入字符串常量池中
再在堆中Eden区创建一个字符串常量池中”XXX“的拷贝对象
2、String的内存分配
在Java中有8大基本类型,和特殊的String
类型,这些类型为了让它们运行过程中更快,更节省内存,都提供了一种常量池的概念,常量池就是一个类似于Java系统级别提供的缓存,8大基本类型的常量池都是系统调节的,而String类型的常量池比较特殊,它的使用方式有两种:双引号声明和new String()
- 在JDK6时字符串常量池在永久代,
- 7及以后,字符串常量池被存放在堆中,因为字符串也是对象,需要被回收,而永久代容量较小,存放大量的字符串会OOM,且永久代的GC频率较低,对无效字符串的回收效率很低,就更容易导致永久代OOM.
由new String(“xx”)产生的字符串对象可以看作普通对象,创建后先存储在堆中的Eden区域
3、String的基本操作
3.1 字符串常量池中存储的字符串唯一
如以下程序,在运行至第一个System.out.println();
时,字符串常量池中已经存在了2518
个字符串,运行至第二个System.out.println();
,字符串常量池中有2528个(换行+10个字符串),以后则不再增加。
public static void main(String[] args) {
System.out.println(); //已经有2518个String了
System.out.println("1"); //String的数量加1
System.out.println("2"); // 2520
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println("10");
System.out.println(); //2529
System.out.println("1"); //字符串的数量不变
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println("10");
}
Java规范要求相同的字符串字面量,应该包含相同的Unicode字符序列,且指向同一个String类实例
3.2 字符串常量池存储在堆中
执行toString()时,将生成的字符串以字面量的形式存储到了堆中的字符串常量池,将该字符串的引用保存到了局部变量表
Heap内存如下:
4、字符串拼接操作
- 常量和常量的拼接结果在常量池,原理是
编译期优化
- 常量池中不会存在相同内容的常量
- 只要其中有一个是常量,结果就在堆中。变量拼接的原理是
StringBuilder
- 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。
public class StringTest2 {
public static void main(String[] args) {
StringTest2 st = new StringTest2();
st.test();
}
public void test() {
String s1 = "Java";
String s2 = "MySQL";
String s3 = "Java" + "MySQL"; //编译器优化,相当于"JavaMySQL"
String s4 = "JavaMySQL";
// 如果字符串拼接的前后过程中出现了变量,则相当于在堆空间中new了一个String()对象
// 具体的内容为拼接的结果 "JavaMySQL"
String s5 = s1 + "MySQL";
String s6 = "Java" + s2;
String s7 = s1 + s2;
/*
String s7 = s1 + s2;的执行细节
1. StringBuilder s = new StringBuilder();
2. s.append("Java");
3. s.append("MySQL");
4. s.toString() -> 约等于 new String()
在JDK1.5 之前使用的是StringBuffer,在1.5之后使用StringBuilder
*/
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()判断字符串常量池中是否存在"JavaMySQL"值,如果存在,返回常量池中"JavaMySQL"的地址值
//如果不存在,在字符串常量池中加载一份"JavaMySQL",将其地址返回
String s8 = s6.intern();
System.out.println(s3 == s8); //true
}
}
4.1 带变量的字符串拼接的底层原理
以上面的代码为例:
String s1 = "a";
String s2 = "b";
String s3 = "c";
String s4 = s1 + "b";
String s5 = s2 + s3;
0 ldc #6 <a>
2 astore_1
//从常量池中取出a, 存放在局部变量表的slot1处
3 ldc #7 <b>
5 astore_2
//从常量池中取出b, 存放在局部变量表的slot2处
6 ldc #8 <c>
8 astore_3
//从常量池中取出c, 存放在局部变量表的slot3处
/** String s4 = s1 + "b"; */
9 new #9 <java/lang/StringBuilder>
12 dup
13 invokespecial #10 <java/lang/StringBuilder.<init> : ()V>
new一个StringBuilder对象,执行其构造方法
16 aload_1
17 invokevirtual #11 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
//从局部变量表的slot1取出值a,调用StringBuilder.append(a)
20 ldc #7 <b>
22 invokevirtual #11 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
//从常量池中取出值b,调用StringBuilder.append(b)
25 invokevirtual #12 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
28 astore 4
// 调用toString()方法,生成一个新的String对象,存放到局部变量表slot4处
/** String s5 = s2 + s3; */
30 new #9 <java/lang/StringBuilder>
33 dup
34 invokespecial #10 <java/lang/StringBuilder.<init> : ()V>
new一个StringBuilder对象,执行其构造方法
37 aload_2
38 invokevirtual #11 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
//从局部变量表的slot2取出值b,调用StringBuilder.append(b)
41 aload_3
42 invokevirtual #11 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
//从局部变量表的slot3取出值c,调用StringBuilder.append(c)
45 invokevirtual #12 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
48 astore 5
// 调用toString()方法,生成一个新的String对象,存放到局部变量表slot5处
50 return
4.2 直接拼接于append的效率比较
public class AppendEfficiency {
public static void main(String[] args) {
AppendEfficiency ae = new AppendEfficiency();
ae.testString(); //8931
ae.testStringBuilder(); //10
ae.testStringBuffer(); //16
}
// 测试String的拼接效率
public void testString() {
long start = System.currentTimeMillis();
String s = "";
for (int i = 0; i < 200000; i++) {
s += "a";
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
// 测试StringBuilder的拼接效率
public void testStringBuilder() {
long start = System.currentTimeMillis();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 200000; i++) {
builder.append("a");
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
// 测试StringBuffer的拼接效率
public void testStringBuffer() {
long start = System.currentTimeMillis();
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < 200000; i++) {
buffer.append("a");
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
对应同样拼接200000
次字符串,String和StringBulider、StringBuffer完全不在一个量级上,最快的是StringBulider
在带变量的直接拼接底层页式通过StringBulider的append()实现
但是200000
次拼接就生成了200000
个StringBuilder对象,每个StringBuilder还需要调用toString(),因此又生成了200000
个String对象。
生成200000
个StringBuilder对象和200000
个String对象的过程也需要时间,且浪费了大量堆空间。并且每次循环前的StringBuilder和String对象失去了引用
,需要被回收
,频繁的GC
又会导致多次STW
,导致整个用户线程执行速度降低
而使用1个StringBuilder或StringBuffer完成拼接能节约数倍的空间和时间
优化建议
:
- 面对大量的字符串拼接,在循环外创建一个StringBuilder,使用append()添加字符串
- StringBuilder初始数字的长度是16,每次容量不够时,就需要进行扩容,每次扩容都是一次拷贝,拷贝也是需要花时间的。如果大概知道需要多少空间,使用StringBuilder的带参构造方法指定容量大小。
5、intern()的使用
对于非字面量声明的String对象,可以使用String类提供的intern()方法,intern()会从字符串常量池中查询当前字符串是否存在,若不存在则将当前字符串放入字符串常量池,并返回其在字符串常量池的地址,如果存在,则直接返回字符串常量池中对应字符串的地址
String str = new String("lcp").intern();
intern()方法确保字符串常量池中存在一份拷贝
这样可以节约内存空间,加快字符串操作的执行速度
在JDK6及之前 intern()方法先检查字符串常量池有无对应字符串对象
无则在字符串常量池中创建新对象,并返回新对象的地址
有则返回字符串常量池中已存在对象的地址
在JDK7及之后 intern()方法先检查字符串常量池有无对应字符串对象
无则将该对象的地址保存在字符串常量池,并返回该地址
有则返回字符串常量池中已存在对象的地址
public static void main(String[] args) {
//先通过new String 在字符串常量池和Eden区中分别存放一份“lcp”
//调用intern方法,在字符串常量池中查找是否存在“lcp”,如果找到,返回字符串常量池中“lcp”的地址
//str就指向了字符串常量池中“lcp”
String str = new String("lcp").intern();
// 以字面量形式声明的字符串放在字符串常量池中,且已存在
// str也指向字符串常量池中“lcp”, 两者相同
System.out.println(str == "lcp"); //true
}
5.1 关于intern的练习
练习1:
String s1 = new String("1"); //这个是new的对象的地址
s1.intern();
String s2 = "1"; //这个是字符串常量池中的地址
System.out.println(s1 == s2); // false
练习2:
String s3 = new String("1") + new String("1"); //s3记录的地址为new String("11")的地址,字符串中不存在"11"
s3.intern(); //在字符串常量池中生成"11",jdk7/8后,因为堆中已经存在了"11",那么字符串常量池中实际保留的是s3的地址
String s4 = "11"; //指向了字符串常量池中的"11"的地址
System.out.println(s3 == s4); // true
6、G1对堆中String的去重
对于很多Java应用做出测试得到一个结果:
- 堆中存活的数据String占了25%
- 堆中存活的重复的String对象有13.5%
- String对象的平均长度是45B
许多大规模Java应用的瓶颈在于内存,测试表明这些类型的应用中,堆中存在大量的重复字符串对象,即str1.equals(str2) = true,堆中重复的String对象是一种内存的浪费
,为此G1垃圾收集器提供了对堆中重复的String对象去重的功能,以减少内存浪费,提高程序性能.
HotSpot VM对堆中重复String去重的操作默认是关闭的,需要手动打开:
-XX:+UseStringDeduplication
,只适合G1
7、补充
7.1 测试new一个String创建了几个对象?
public class StringNewTest {
public static void main(String[] args) {
/** new String("ab") 创建了几个对象? 2个(1个String对象 + 1个字符串常量池对象)*/
/**
* 0 new #2 <java/lang/String> 创建一个String对象,存在于堆中
* 3 dup
* 4 ldc #3 <ab> 在字符串常量池中也有一个ab对象
* 6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
* 9 astore_1
*/
String s1 = new String("ab");
}
}
7.2 new String(“a”) + new String(“b”) 创建了几个对象?
public static void main(String[] args) {
/** new String("a") + new String("b") 创建了几个对象?
* 6个(1个StringBuilder + 2个String + 2个字符串常量池 + 1个返回的toString
* 字符串常量池中不存在 "ab"
*/
/**
* new #5 <java/lang/StringBuilder> 创建一个StringBuilder对象
* 13 dup
* 14 invokespecial #6 <java/lang/StringBuilder.<init> : ()V>
* 17 new #2 <java/lang/String> 创建一个String对象
* 20 dup
* 21 ldc #7 <a> 常量池中存在一个 "a"对象
* 23 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
* 26 invokevirtual #8 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
* 29 new #2 <java/lang/String> 创建一个String对象
* 32 dup
* 33 ldc #9 <b> 常量池中存在一个 "b"对象
* 35 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
* 38 invokevirtual #8 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
* 41 invokevirtual #10 <java/lang/StringBuilder.toString : ()Ljava/lang/String;> 再返回一个对象
* 44 astore_2
*/
String s2 = new String("a") + new String("b");
}