文章目录
String 的基本特性
-
字符串,用""引起来表示
-
声明为final的,不可被继承
-
实现了Serializable接口,表示字符串支持序列化,实现了Comparable接口,表示String可以比较大小。
-
String在JDK8以及以前内部定义了final char[]存储字符串数据 ,JDK9之后改用了final byte []。
- 修改是因为部分字符集使用char会导致一个字节的空间浪费。
Motivation
The current implementation of the
String
class stores characters in achar
array, using two bytes (sixteen bits) for each character. Data gathered from many different applications indicates that strings are a major component of heap usage and, moreover, that mostString
objects contain only Latin-1 characters. Such characters require only one byte of storage, hence half of the space in the internalchar
arrays of suchString
objects is going unused.Description
We propose to change the internal representation of the
String
class from a UTF-16char
array to abyte
array plus an encoding-flag field. The newString
class will store characters encoded either as ISO-8859-1/Latin-1 (one byte per character), or as UTF-16 (two bytes per character), based upon the contents of the string. The encoding flag will indicate which encoding is used. -
String是不可变的字符序列,
-
当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值
-
当对现有的字符串进行连续操作时,也需要重新指定内存区域赋值,不能使用原有的Value进行赋值
-
当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
String s1 = "value"; String s2 = "value";//此时s1和s2指向的是同一个地址 s1+=" of"; //此时s1指向新的字符串。
-
-
通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。
-
字符串常量池中不会存储相同内容的字符串
-
String的String Pool是一个固定大小的HashTable,默认值大小长度是1009(JDK6)如果String Pool的String很多,会造成Hash冲突,导致链表会很长,调用String.intern时性能会大幅度下降
-
-XX:StringTableSiez
可设置StringTable大小 -
JDK7中的默认值是60013
-
JDK8开始,1009是可设置的最小值,后续JDK版本不同,默认值也有所区别。
String的内存分配
- 常量池类似一个Java系统级别提供的缓存,八种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。主要是用方法有两种
- 直接使用双引号声明出来的String对象会存储在常量池中。如:
String info = "test String"
- 非双引号声明的String,可以使用
intern()
方法
- 直接使用双引号声明出来的String对象会存储在常量池中。如:
- JDK 6及之前,字符串常量池存放在永久代
- Java7中将字符串常量池位置调整到了Java堆内
- 所有字符串都保存在堆中,和其它普通对象一样,这样可以在进行调优时仅调整堆大小
- 字符串常量池概念原本使用的比较多,但是该变动使得有足够的理由重新考虑在Java7中使用
String.intern()
- Java 8 永久代改为元空间,字符串常量池依然在堆中。
为什么调整?
- permSize(永久代空间)默认比较小,大量字符串可能导致OOM
- 永久代GC频率低
String的基本操作
- Java语言规范里要求完全相同的字符串字面量,应该包含同样的Unicode字符串序列(包含同一份码点序列的常量),并且必须是指向同一个String类的实例。
字符串拼接操作
- 常量与常量的拼接结果在常量池,原理是编译期优化
- 常量池中不会存在相同内容的常量
- 只要其中一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
- 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
public void test(){
String s1 ="1"+"2"+"3";//也存在常量池中,生成字节码文件时,将其等同于“123”,放入常量池中。
String s2 = "123"; //一定存放在常量池中,地址给s2
System.out.println(s1==s2);//true
System.out.pringln(s1.equals(s2));//true
}
public void test(){
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的地址
//如果字符串畅两会次不存在,则在常量池中加载一份字符串,并返回此类对象地址。
String s8 = s6.intern();
System.out.println(s3 == s8); //true
}
S1+S2的底层细节
new StringBuilder()
注意,jdk5之前使用的是StringBuffer- 调用
append(s1)
方法 - 调用
append(s2)
方法 toString()
类似new String(“ab")
如果某个变量声明时加了final,那么实际上就不是变量了,和其它常量拼接时依然会存放到常量池中
intern()的使用
- new String(”ab“)会创建几个对象?
- 两个,一个是new关键字在堆空间中创建,另一个是字符串常量池中的对象。
- new String (“a")+new String (”b“)呢?
- new StringBuilder
- new String(“a”)
- 常量池中的“a"
- new String(“b”)
- 常量池中的“b”
- StringBuilder的toString()
- 注意:toString()的调用,不会在字符串常量池中生成“ab”
public class StringIntern{
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);
}
}
jdk6 or jdk 7/8
jdk6:
public class StringIntern{
public static void main(String []args){
String s = new String("1");//堆空间中创造了一个“1”,常量池中也创造了一个“1”
s.intern(); //调用此方法前,常量池中存在“1”
String s2 = "1"; //
System.out.println(s == s2); //false
String s3 = new String("1")+new String("1");// 记录的地址为 new String(11)
//执行完上一段代码后,字符串常量池中字符串是否存在11 与版本有关
s3.intern(); //JDk6创建了一个新的对象,也就有新的地址
String s4 = "11";
System.out.println(s3==s4); //false
}
}
JDK7/8:
public class StringIntern{
public static void main(String []args){
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2); //false
String s3 = new String("1")+new String("1");
//执行完上一段代码后,字符串常量池中字符串不存在“11”
s3.intern(); //在字符串常量池中生成“11”,JDK7中把常量池放在了堆空间中,堆空间有对象的话,常量池会引用堆空间new的"11"的地址
String s4 = "11";//使用的是上一行代码执行时,常量池生成的“11”的地址
System.out.println(s3==s4) ;//true
}
}
- 注意:JUNIT中测试和Main方法测试结果可能不同
拓展
//jdk8
public class StringIntern{
public static void main(String []args){
String s3 = new String("1")+new String("1"); //new String("11")
//s4与s3.intern()互换位置
String s4 = "11"; //在字符串常量池中生成对象“11”,不再是引用
s3.intern(); //实际并没有作用,常量池中已经存在“11”,但是不会改变S3地址
s5 = s3.intern();
System.out.println(s3==s4);//false
System.out.println(s3==s5);//true
}
}
总结:
- JDK6中,这个字符串对象将会被放在串池
- 如果串池中有,则不会放入。返回已有的串池中的对象的地址
- 若没有,则会把此对象复制一份,放入串池,并返回串池中的对象地址
- JDK7开始,资格字符串对象尝试放入串池
- 如果串池中有,则不会放入。返回已有的串池中的对象的地址
- 若没有,则会把此对象的引用复制一份,放入串池,并返回串池中的引用地址
public class StringTest{
public static void main(String []args){
String s = new String("a")+new String("b");
//此时常量池并不存在“ab"
String s2 = s.intern();/*jdk 6 中,会在串池中创建一个字符串“ab",地址给s2
jdk 7 中,不创建“ab”,而是创建一个引用指向new String("ab");
*/
System.out.ptintln(s2=="ab");//jdk6:true jdk8:true
System.out.ptintln(s2=="ab");//jdk6:false jdk8:true
}
}
引申问题
public class StringTest{ public void static main(String [] args){ String x = "ab";; String s = new String("a")+new String ("b"); String s2 = s.intern(); System.out.println(s2==x);//true System.out.println(s==x);//true }}
public class StringTest2{ public static void main(String [] args){ String s1 = new String("ab");//执行完成后,在常量中会生成“ab" s1.intern(); String s2 = "ab"; System.out.println(s1==s2);//true }}
G1中的String去重操作
去重操作:常量池中本身不会存在重复数据,去重操作不是常量池中的操作。
String s1 = new String("11");String s2 = new String("11");
说明
-
堆存活数据集合里String占了25%;
-
对存活数据集合里重复的String有13.5%;
-
String对象的平均长度是45;
-
许多大规模的Java应用瓶颈在于内存,测试表明:这些类型的应用里,Java堆中,有袢的对象是重复的,即
string1.equals(string2)==true
-
G1垃圾收集器中实现自动持续对重复的String对象去重,可以避免内存浪费。
实现
- 当垃圾收集器工作的时候,会访问堆上存活的对象。对每一个访问的对象都会检查是否候选的要去重的String对象
- 如果是,吧这个对象的引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重置它引用的String对象。
- 使用一个HashTable来记录所有被String对象使用的不重复的Char数组。去重的时候,会查这个HashTable,来看堆上是否已经存在一个一模一样的char数组。
- 如果存在,String对象会被调整引用那个数组,释放对原来的数组的引用。最终会被垃圾回收器回收掉。
- 如果查找失败,char数组会被插入到HashTable,这样以后就可以共享数据了。
命令行
UseStringDeduplication(bool):
开启String去重,默认不开启,需手动开启PrintStringDeduplicationStatistics(bool)
打印详细的去重信息StringDeduplicationAgeThreshold(uintx)
达到这个年两的String对象被认为是去重的候选对象。