文章目录
StringTable
String的基本特性
-
String:字符串,使用一对“ ”引起来表示
-
String声明为final的,不可被继承
-
String 实现了Serializable接口:表示字符串是支持序列化的;实现了Conparable接口:表示String可以比较大小
-
JDK8即以前定义final char[] valuefinal char[] value用于存储字符串数据,JDK9改为byte[](节约了一些空间,同时StringBUffer等内部也修改了)
-
String:代表不可变的字符串序列,简称:不可变性
- 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值
- 当对现有字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
- 当调用String的replace()修改指定字符串或字符时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
-
通过字面量声明在字符串常量池中
字符串常量池中不会存储相同内容的字符串
String的String Pool是一个大小固定的HashTable,默认长度是1009.如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表很长,而链表长了以后会造成String.intern时性能大幅下降
使用-XX:StringTableSize可以设置StringTable的长度
jdk6中StringTable时固定的,就是1009的长度,所以如果常量池中字符串过多会导致效率下降很快。StringTableSize设置没有要求
jdk7中StringTable默认是60013。StringTableSize设置没有要求
jdk8开始,设置StringTable长度,1009是可设置的最小值
String的内存分配
常量池就类似一个Java系统级别提供的缓存。8中基本数据类型都是系统协调的,String类型的常量池比较特殊。它主要使用方法有两种
- 直接声明String对象会直接存储在常量池中
- 使用intern()
Java6之前,字符串常量池放在永久代中
Java7中将字符串常量池放在Java堆中
Java8元空间,字符串常量在堆中
StringTable为什么要调整?
①permSize默认空间比较小②永久代垃圾回收频率低
String的基本操作
public class byte_char {
public static void main(String[] args) {
System.out.println();//2228
System.out.println("1");//2229
System.out.println("2");//2230
System.out.println("3");//2231
System.out.println("1");//2232
System.out.println("2");//2232
System.out.println("3");//2232
}
}
Java语言规范里要求完全相同的字符串字面量,应该包含同样的Unicode字符序列(包含同一份码点序列的常量),并且必须指向同一个String类实例
public class byte_char {
public static void main(String[] args) {
int i = 1;
Object obj = new Object();
byte_char byte_char = new byte_char();
byte_char.foo(obj);
}
private void foo(Object para) {
String s = para.toString();
System.out.println(s);
}
}
字符串拼接操作
- 常量与处理的拼接结果在常量池,原理是编译期优化
- 常量池不会存在相同内容的常量
- 只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
- 如果拼接的结果调用inter(),则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
@Test
public void test1() {
String s1 = "a" + "b" + "c";//编译期优化
String s2 = "abc";
System.out.println(s1.equals(s2));//true
System.out.println(s1 == s2);//true
}
@Test
public void test2() {
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop";//编译期优化
String s5 = s1 + "hadoop";//如果出现了变量,则相当于在堆中间new对象,具体内容为拼接结果
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
//s6对应的值,如果在字符串常量池中存在,则返回常量池中对应的地址;如果不存在,则在堆中创建,再将结果放入常量池中
String s8 = s6.intern();//true
System.out.println(s3 == s8);
}
@Testpublic void test3() { String s1 = "a"; String s2 = "b"; String s3 = "ab"; /* s1 + s2细节如下 ①StringBuilder s = new StringBuilder(); ②s.append("a"); ③s.append("b"); ④s.toString() --->约等于 new String("ab") 在jdk5.0之后使用的StringBuilder,在jdk5.0之前使用StringBuffer */ String s4 = s1 + s2; System.out.println(s4 == s3);//false}
@Testpublic void test4() { final String s1 = "a"; final String s2 = "b"; String s3 = "ab"; //字符串拼接,不一定是StringBuilder,如果拼接符左右两边都是字符串常量或者是常量引用,则仍然使用编译期优化,非StringBuilder模式 //针对于final修饰基本数据类型,引用数据类型,类,方法若能使用就建议使用final,因为用final修饰在编译期间就会有值,即显示赋值 String s4 = s1 + s2;//此时理解为常量 System.out.println(s4 == s3);//true}
StringBuilder和String的+
体会
- 效率
- 对于String的拼接而言每次循环都要创建String,StringBuilder,
- 但对于StringBuilder自始至终只会创建一个StringBuilder
- 内存
- 对于String的拼接,每次循环都要创建String,StringBuilder,占用的内存较大;并会有发生GC的可能还需要额外的开销
- StringBuilder的改进,避免不断扩容;在实际开发中,如果前后添加某个字符串,不高于某个限定值,建议使用有参构造器StringBuilder s = new StringBuilder(highLevel)//new char[highLevel]
@Testpublic void test5() { long start = System.currentTimeMillis(); // method1(100000);//花费4692ms method2(100000);//花费15ms long end = System.currentTimeMillis(); System.out.println("花费" + (end - start) + "ms");}public void method1(int count) { String src = ""; for (int i = 0; i < count; i++) { src = src + "a";//每次循环都要创建String,StringBuilder }}public void method2(int count) {//自始至终只会创建一个StringBuilder StringBuilder s = new StringBuilder(); for (int i = 0; i < count; i++) { s.append("a"); }}
inter()的使用
如果不是用双引号声明的String对象,可以使用String提供的inter();inter方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中
intern()就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(String Intern Pool)
public class TestIntern { public static void main(String[] args) { String str1 = new String("ab");//在堆空间中创建,并在字符串常量池中也会创建 str1.intern();//此时字符串常量池中已经有"ab" String str2 = "ab";//指向字符串常量池中 System.out.println(str2==str1); //jdk6/7/8 false //只会在堆空间中创建,根据字节码分析,StringBuilder中的toString()中的new Stirng("12"),不会在字符串常量池中创建"12" String str3 = new String("1")+new String("2"); str3.intern();//jdk6 会在字符串常量池中创建新的对象 //jdk7/8 在字符串常量池中创建指向堆空间的地址值,此时堆和字符串常量池是同一个地址 String str4 = "12";//指向字符串常量池 System.out.println(str3==str4);//jdk6 false【一个指向堆,一个指向字符串常量池】 jdk7/8 true【此时堆和字符串常量池是同一个地址】 }}
因为new String(“ab”)会在堆中创建并去字符串常量池中创建,str2是指向堆中引用,str1指向字符串常量池,因此返回false。但是对于new String(“a”)+new String(“b”)只会在堆中创建,不会在字符串常量池中创建,即字符串常量池中没有"ab",str2.intern()会在字符串常量池中创建,并返回地址,String str1 = “ab”;直接从字符串常量池中中获取,所以二者其实都是从字符串常量池中中获取的,因此返回true
面试题
new Stirng(“ab”)会创建几个对象?会在字符串常量池中中创建"ab"
new String(“a”) + new Stirng(“b”)呢?不会在字符串常量池中创建"ab"
/** * 0 new #2 <java/lang/String> 在堆中创建对象 * 3 dup * 4 ldc #3 <abc> 在字符串常量池中创建 */public class TestString { public static void main(String[] args) { String abc = new String("abc"); }}
/** * 0 new #2 <java/lang/StringBuilder> ①在堆中创建对象StringBuilder * 3 dup * 4 invokespecial #3 <java/lang/StringBuilder.<init>> * 7 new #4 <java/lang/String> ②在堆中创建对象String * 10 dup * 11 ldc #5 <a> ③在字符串常量池中创建"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> ⑤在字符串常量池中创建"b" * * 深入分析:将创建的对象append到StringBuilder中,再调用toString(), * 会再创建一个String对象,即new String("ab"),但是在字符串常量池中并没有生成"ab" */public class TestString { public static void main(String[] args) { String s = new String("a") + new String("b"); }}
public class TestIntern1 { public static void main(String[] args) { String str3 = new String("1")+new String("1");//在堆中创建一个"11",此时字符串常量池中没有 String str4 = "11";//在字符串常量池中创建"11" String str5 = str3.intern();//返回字符串常量池中已存在的地址 System.out.println(str3==str4);//false System.out.println(str4 == str5);//true }}
intern()的使用:jdk6 vs jdk7/8
- jdk6中,将这个字符串对象尝试放入串池
- 如果串池中有,则不会放入。返回已有的串池中的对象的地址
- 如果没有,会把对象复制一份,放入串池,并返回串池中的对象地址
- jdk7中,将这个字符串对象尝试放入串池
- 如果串池中有,则不会放入。返回已有的串池中的对象的地址
- 如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址
public class TestIntern1 { public static void main(String[] args) { String str3 = new String("1")+new String("1");//在堆中创建一个"11",此时字符串常量池中没有 String str5 = str3.intern(); System.out.println(str3=="11");//jdk7/8 true jdk6 false System.out.println(str5 == "11");//jdk7/8true jdk6 true }}
public class TestIntern2 { public static void main(String[] args) { String s1 = new String("ab");//会再字符串常量池中创建 false// String s1 = new String("a") + new String("b");//不会再字符串常量池中创建 true s1.intern();//jdk7/8在字符串常量池中创建指向堆地址的值 String s2 = "ab"; System.out.println(s1 == s2); }}
intern()的效率测试
对于程序中存在大量的字符串,尤其其中存在很多重复的字符串时,使用intern()可以节省内存空间
StringTable的垃圾回收
G1中的String去重操作
Java堆中存活的数据集合差不多25%是String对象
堆上存在重复的String对象必然是一种内存的浪费,因此去重操作是对堆操作的
UseStringDeduplication (bool):开启String去重,默认是不开启的,需要手动开启