String基本特性
- String字符串,使用一对""引起来表示
- String s1 = "lotus"; //字面量定义方式
- String s2 = new String("hello")
- String声明为final,不可被继承
- String实现了Serializable接口,表示字符串支持序列化,实现Comparable接口,表示String可以比较大小
- String在JDK8以前内部定义了final char[] value用于存储字符串数据,jdk9改为byte[]
- String代表不可变字符序列,称不可变性
- 当对字符串重新赋值时,重写指定内存区域赋值,不能使用原有的value进行赋值
- 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
- 当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
- 通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中
- 字符串常量池不会存储相同内容的字符串
- 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,这些类型为了使它们运行过程中速度更快,更节省内存,都提供了一种常量池的概念
- 常量池类似于一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String的常量池比较特殊,主要使用方法有两种
- 直接使用双引号声明出的String对象会直接存储在常量池中
- 不使用双引号声明的String对象,可以使用String提供的intern()方法
- Java6及以前,字符串常量池存放在永久代
- Java7中对字符串池的逻辑做了改变,将字符串常量池位置调整到Java堆内
- 所有的字符串都保存在堆中,和其他普通对象一样,这样可以让你在进行调优应用时仅需调整堆大小就可以了
- 字符串常量池概念原本使用得比较多,但是这个改动使我们有足够的理由重新考虑使用String.intern()
- Java8元空间,字符串常量池在堆中
字符串拼接操作
- 常量与常量拼接结果在常量池,原理是编译期优化
java
@Test
public void test1() {
String s1 = "a" + "b" + "c"; //等同于"abc",//编译期优化
String s2 = "abc";
//最终.java编译成.class,再执行.class
// String s1 = "abc"; String s2 = "abc"
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
}
//执行结果
true
true
- 常量池中不会存在相同内容的常量
- 只要其中有一个是变量,结果就在堆中,变量拼接原理是StringBuilder
java
@Test
public 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
//判断s6的值是否在常量池中,如果查到则直接返回常量池中的地址,如字符串常量池中不存在,则在常量池中加载一份,并返回此对象地址
String s8 = s6.intern();
System.out.println(s3 == s8); //true
}
@Test
public void test3() {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
/**
* s1 + s2执行细节
* StringBuilder sb = new StringBuilder();
* sb.append("a")
* sb.append("b")
* sb.toString()----------
* @Override---StringBuilder
* public String toString() {
* // Create a copy, don't share the array
* return new String(value, 0, count);
* }
*
* 在jdk5.0以后使用StringBuilder,5.0之前使用StringBuilder
*/
String s4 = s1 + s2;
System.out.println(s3 == s4); //false
}
@Test
public void test4() {
/**
* 1.字符串拼接不一定使用StringBuilder
* 如果拼接符号左右两边都是字符串常量或常量引用,仍使用编译期优化,即非StringBuilder
* 2.针对于final修饰类、方法、基本数据类型、引用数据类型的量的结构时,能使用上final时使用上
*/
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4); //true
}
/**
* 通过StringBuilder的append方式添加字符串效率要远高于使用String的字符串拼接方式
* 详情:①使用StringBuilder的append方式,自始至终只创建一个StringBuilder对象
* 使用String拼接的方式,每次循环会创建一个StringBuilder和一个String
* ②使用String拼接方式,内存由于创建了较多的StringBuilder对象,占用内存大,GC耗时
* 改进空间:在实际开发中,如果能基本确定要前前后后添加的字符串长度不高于某个限定值highLevel情况下,建议使用构造器创建StringBuilder
* StringBuilder sb = new StringBuilder(highLevel)
*/
@Test
public void test5() {
long start = System.currentTimeMillis();
//method1(100000); //花费时间:3641
method2(100000); //花费时间:0
long end = System.currentTimeMillis();
System.out.println("花费时间:" + (end - start));
}
private void method1(int highLevel) {
String src = "";
for (int i = 0; i < highLevel; i++) {
src += "a"; //每次循环创建一个StringBuilder,String
}
}
private void method2(int highLevel) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < highLevel; i++) {
sb.append("a");
}
}
- 如果拼接的结果调用intern()方法,则该去将常量池中还没有的字符串对象放入池中,并返回些对象地址
intern的使用
- JDK6中,将这个字符串对象尝试放入串池
- 如果串池中有,则并不会放入,返回已有的串池中的对象地址
- 如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址
- JDK7起,将这个字符串对象尝试放入串池
- 如果串池中有,则并不会放入,返回已有的串池中的对象地址
- 如果没有,会把此对象的引用地址复制一份,放入串池,并返回串池中的对象地址
java
/**
* 如何保证变量s指向的是字符串常量池中的数据?
* 方式①:String s = "lotus";//字面量定义的方式
* 方式②:调用intern()方法
* String s = new String("lotus").intern()
* String s = new StringBuilder("lotus").toString().intern()
*/
public class StringIntern {
public static void main(String[] args) {
/**
* new String("1"),会创建两个对象
* 对象1:new String("1")
* 对象2:常量池中"1"
*/
String s = new String("1"); //s指向堆空间创建的字符串对象地址
s.intern();
String s2 = "1"; //s2指向常量池中对象地址
System.out.println(s == s2); //jdk6:false,jdk7/8:false
/**
* new String("1") + new String("1")会创建两个对象
* 对象1:new StringBuilder
* 对象2:new String("1")
* 对象3:常量池中"1"
* 对象4:new String("2")
* 对象5:常量池中"2"
* 深入剖析:StringBuilder的toString()
* 对象6:new String("12")
* 强调,toString()调用,在常量也中没有生顾"12"
*/
String s3 = new String("1") + new String("2"); //s3变量地址为new String("12")
/**
* 在字符串常量池中生成"12"
* jdk6:创建一个新的对象"12",也就有新的地址
* jdk7/8:此时常量池中并没有创建"12",而是创建一个指向堆空间的指针
*/
s3.intern();
String s4 = "12"; //使用上一行代码执行时,在常量池中生成的"12"的地址
System.out.println(s3 == s4); jdk6:false,jdk7/8:true
}
}
G1的String去重操作
- 许多Java应用做的测试得出以下结果
- 堆存活数据集合里面String对象占了25%
- 堆存活数据集合里面重复的String对象有13.5%
- String对象的平均长度是45
- 许多大规模的Java应用的瓶颈在于内存,测试表明,在这些类型的应用里,Java堆中存活的数据集合差不多25%是String对象 ,更进一步,这里面差不多一半String对象是重复的,重复的意思是说string1.equals(string2)=true,堆上存在重复的string对象必然是一种内存资源浪费,这个项目将在G1垃圾收集器中实现自动持续对重复的String对象进行去重,这样就避免浪费内存
- 实现
- 当垃圾收集器工作的时候,会访问堆上存活的对象,对每一个访问的对象都会检查是否是候选的要去重的String对象
- 如果是,把这个对象的一个引用插入到队列中等待后续的处理,一个去重的线程在后台运行,处理这个队列,处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的String对象
- 使用一个hashtable来记录所有的被String对象使用的不重复的char数组,当去重的时候,会查看这个hashtable,不看堆上是否已经存在一个一模一样的char数组
- 如果存在,String对象会被调整引用那个数组,释放对原来的数组的引用,最终被垃圾收集器回收
- 如果查看失败,char数组会被插入到hashtable,这样以后的时候就可以共享这个数组
- 命令行选项
- UseStringDeduplication (bool):开启String去重,默认是不开启的,需要手动开启
- PrintStringDeduplicationStatistics(bool):打印详细的去重统计信息
- StringDeduplicationAgeThreshold(uintx):达到这个年龄的String对象被认为是去重的候选对象