String的基本特性
- String是字符串,声明为final类不可被继承,实现Serializable接口(支持序列化),实现Comparable接口(可比大小),实现CharSequence接口,是不可变的;
public final class String implements java.io.Serializable, Comparable<String>, CharSequence - JDK8内部定义了final char[] value用于存储字符串数据,JDK9类型改为byte[];
- 存储在字符串常量池(常量池中不存在两个相同的字符串);
- String有不可变性,对String的重新赋值实际上底层是重新
new一个String,因为String底层是数组,数组在编译的时候长度已经确定了; - 通过字面量的方式给String赋值,String将创建在字符串常量池中(String str = "abc";) ,返回常量池中的对象
- 通过new的构造方法也会在常量池中创建对象,同时还会再堆空间中创建对象,但是会返回堆空间中的对象;
StringPool(字符串常量池)
StringPool简介
StringPool底层是一个固定大小的HashTable(数组+链表)
设置StringPool的HashTable长度:-XX:StringTableSize
jdk6的StringPool默认长度时1009(没有限制要求)
jdk7的StringPool默认长度时60013(没有限制要求)
jdk8的StringPool默认长度是60013(最小值限制为1009)
字符串常量池不会存储相同的字符串
注意:StringPool的String多了就会产生Hash冲突,调用String.intern时性能下降。
StringPool的内存分配
Java语言中的8种数据类型和String类,为了在运行的时候响应更快,更节省内存,提供了一种常量池的概念,8种数据类型的常量池是系统协调的,String类型的常量池比较特殊,有两种创建方法:
- 字面量声明(双引号括起来的都可以算字面量声明:System.out.printf("abc"); 或 String str = "abc";),会直接创建在字符串常量池中
- 非字面量声明,使用String.intern()方法加入字符串常量池中,返回常量池中的对象
StringPool的位置
StringPool的位置在jdk6和jdk7之间有重大的位置调整,调整结构图如下:
到了JDK8或以上方法区的实现方式变为元空间,其他的没有改变。
调整原因
调整字符串常量池最重要的原因有两点:
- 在JDK8或以前,方法区的实现方式是永久代,而永久代是在JVM里面分配的,32位机器永久代默认最大空间是64M,64位机器永久代默认最大空间是82M,可见是比较小的,对于程序中大量应用的String字符串,容易出现OOM;
- 永久代属于方法区的实现方式,垃圾回收的频率很低;
String的拼接操作
拼接操作的特点
- 常量的拼接(字面量) 结果是只放在字符串常量池中的,原理是编译期优化;
public void stringTest1(){ String s1 = "a" + "b" + "c"; String s2 = "abc"; /** * 在java文件编译成class文件后,会对s1和s2进行编译期优化 * 打开编译后的class文件可以看到 * String s1 = "abc"; * String s2 = "abc"; */ System.out.println(s1 == s2);//true System.out.println(s1.equals(s2));//true } - 只要其中一个是变量(包括两个或以上都是变量),结果就在堆中(常量池外,且常量池不会有);
public void stringTest2(){ String s1 = "Java"; String s2 = "Hotspot"; String s3 = "JavaHotspot"; String s4 = "Java" + "Hotspot"; String s5 = s1 + "Hotspot"; String s6 = "Java" + 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 }
字符串拼接的底层原理
字面量拼接
字面量拼接也就是常量拼接,可以以拼接特点的第一点的代码来说明,主要是编译器的优化,且结果只存在字符串常量池。
public void stringTest1(){
String s1 = "a" + "b" + "c";
String s2 = "abc";
/**
* 在java文件编译成class文件后,会对s1和s2进行编译期优化
* 打开编译后的class文件可以看到
* String s1 = "abc";
* String s2 = "abc";
*/
System.out.println(s1 == s2);//true
System.out.println(s1.equals(s2));//true
}
变量拼接
变量拼接(只要其中有一个或以上的变量),结果就会在堆中,而不会在字符串常量池中。
public static void stringTest3(){
String s1 = "a";
String s2 = "b";
String s3 = "ab";
/**
* s1 + s2 的执行细节如下:
* 1. StringBuilder s = new StringBuilder();
* s.append("a");
* s.append("b");
* s.toString(); --> 约等于 new String("ab")
*/
String s4 = s1 + s2;
System.out.println(s3 == s4);//false
}
以拼接特点的第二点来说明,s4是由变量拼接而成的,所以结果不会再常量池中 (StringBuilder的toString方法中new的String是没有放入字符串常量池中的),而是在堆中,所以s3==s4为false
建议
对于final修饰的类,方法,基本数据类型,引用数据类型的量的结构时,可以使用final就使用final,因为在类加载的链接的第二个阶段准备,final修饰的量就直接显示初始化了。
效率
使用拼接符号进行拼接底层其实是使用StringBuilder调用append方法进行拼接,每一次的拼接都要new一个StringBuilder和String,内存占用大,而且GC的时候花费更多的时间。而直接使用StringBuilder进行拼接,效率会比使用拼接符号高很多。
StringBuilde使用优化建议:创建StringBuilder的时候调用其构造函数,减少使用时的扩容,前提时需要确定StringBuiler的内存空间
StringBuiler s = new StringBuilder(highLevel);//new char[highLevel]
String的intern()方法
方法说明
调用intern()方法,会通过equals方法比较,查询字符串常量池中有没有相同的String对象
- 如果有,返回该相同对象字符串常量池中的地址
- 如果没有,创建一个对象在常量池中,返回字符串常量池中的对象(JDK6和JDK7或以上的有区别)
如何保证变量指向的时字符串常量池中的数据?
- 字面量定义:String s = "abc";
- 调用intern()方法:
String s = new String("abc").intern(); String s = new StringBuilder("abc").toString().intern();
JDK6和JDK7/8或以上intern方法的区别
JDK6:将字符串对象放入字符串常量池中
-
如果字符串常量池有,则不会放入,返回已有的池中的对象
-
如果字符串常量池没有,则深拷贝一份对象,放入池子中,并返回池子中的对象地址(地址和堆中的不一样)
JDK7/8或以上:将字符串对象放入字符串常量池中
-
如果字符串常量池中有,则不会放入,返回已有的池中的对象
-
如果字符串常量池中没有,则会把对象的地址复制一份,放入池中,返回对象引用地址(地址和堆中的一样)
intern()方法的目的
对于大量的重复的字符串对象,使用intern方法可以使返回的对象指向常量池中的字符串对象,而new出来的字符串对象会因为没有使用被回收,节省空间
G1垃圾回收器中String的去重操作:去String底层char/byte数组的重复,但是要保证String的不变性