目录
一、String的基本特性
二、String的内存分配
2.1、String内存分布图
三、字符串拼接操作
3.1、字符串拼接操作底层原理
3.2、拼接操作与append操作效率对比
四、intern()方法
4.1、intern()效率
五、StringTable的垃圾回收
1、String字符串,使用一对""引起表示
2、String声明为Final的,不可被继承
3、String实现了Serializable接口:表示字符串是支持序列化的。实现Comparable接口:表示String可以比较大小。
4、String字符串在JDK8及以前内部定义了final char[] value用于存储字符串数据。jdk9时改为byte[]。
5、String代表不可变的字符串序列。①、当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。②、当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。③、当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
6、通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。
参考代码:
public class StringTest {
@Test
public void test1(){
String s1 = "a";
String s2 = "a";
s2 = "b";
System.out.println(s1 == s2);//false
System.out.println(s1);//a
System.out.println(s2);//b
}
@Test
public void test2(){
String s1 = "a";
String s2 = "a";
s2 += "b";
System.out.println(s1);//a
System.out.println(s2);//ab
}
@Test
public void test3(){
String s1 = "a";
String s2 = s1.replace('a','b');
System.out.println(s1);//a
System.out.println(s2);//b
}
}
面试题:
public class StringTest02 {
String s1 = new String("hello");
char[] chars = {'t','o','m'};
public void change(String s1,char chars[]){
s1 = "test ok";
chars[0] = 'a';
}
public static void main(String[] args) {
StringTest02 str = new StringTest02();
str.change(str.s1,str.chars);
System.out.println(str.s1);//”hello“
System.out.println(str.chars);//”aom“
}
}
7、字符串常量池是不会存储相同内容的字符串。
①、String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009.如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是调用String.intern时性能会大幅度下降。
参考代码:
public class StringTest04 {
public static void main(String[] args) {
try {
FileWriter fileWriter = new FileWriter("hello.txt");
for (int i = 0; i < 100000; i++) {
int length = (int) (Math.random() * (10) + 1);
fileWriter.write(getString(length) + "\n");
}
fileWriter.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static String getString(int length){
String str = "";
for (int i = 0; i < length; i++) {
int num = (int) (Math.random() * (90 - 65 + 1) + 65) + (int) (Math.random() * 2) * 32;
str += (char)num;
}
return str;
}
}
public class StringTest03 {
public static void main(String[] args) {
//参数设置:-XX:StringTableSize=1009
// System.out.println("String参数设置开始");
// try {
// Thread.sleep(100000);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
BufferedReader bufferedReader = null;
try {
bufferedReader = new BufferedReader(new FileReader("hello.txt"));
long start = System.currentTimeMillis();
String data;
while ((data = bufferedReader.readLine()) != null){
data.intern();
}
long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start));//参数1009.时间91ms。参数1000009,时间35ms
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
if (bufferedReader != null){
try {
bufferedReader.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
②、使用-XX:StringTableSize可设置StringTable的长度。
③、在jdk6中的StringTable的是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。StringTableSize设置没有要求。
④、在jdk7中,StringTable的默认长度是60013,jdk8及以后1009是设置的最小值。
以jdk8测试:
1、Java语言中有八大基本数据类型和特殊的String类型,这些类型为了使它们在运行过程中速度更快,更节省内存,都提供了一种常量池概念。
2、常量池类似一个Java系统级别提供缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。
①、直接使用双引号声明出来的String对象会直接存储在常量池中。
②、不使用双引号声明的String对象,可以使用String提供的intern()方法。
3、Java6及以前,字符串常量池存放在永久代中。
4、Java7中将字符串常量池的位置调整到Java堆中。
①、所有的字符串都保存在堆中,和其他普通对象一样,这样可以在进行调优应用时仅需要调整堆大小就可以了。
②、字符串常量池概念原本使用的比较多,但改动后可以重新考虑在Java7中使用String.intern()
5、Java8元空间,字符串常量在堆。
1、常量与常量的拼接结果是在常量池,原理是编译期优化。
2、常量池中不会存在相同内容的常量。
3、只要其中一个是变量,结果就在堆中。变量的拼接的原理是StringBuilder。
4、如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。
参考代码:
@Test
public void test1(){
String s1 = "a" + "b" + "c";
String s2 = "abc";
System.out.println(s1 == s2);//true
System.out.println(s1.equals(s2));//true
}
@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
//intern(),判断字符串常量池中是否存在javaeehadoop值,有就返回该值地址,没有就重新加载一份。
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 s = new StringBuilder();
* ②、s.append("a")
* ③、s.append("b")
* s.toString() ---> 约等于 new String("ab");
*/
String s4 = s1 + s2;
System.out.println(s3 == s4);//false
}
@Test
public void test4(){
/**
* 字符串拼接操作不一定使用StringBuilder()
* 如拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译期优化
* 针对于final修饰的类、方法、基本数据类型,引用数据类型的量的结构时,能使用final时建议使用
*/
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);//true
}
参考代码:
@Test
public void test5(){
/**
*通过StringBuilder的append()的方式添加字符串的效率要远高于字符串拼接
* ①、StringBuilder的append()方式,只需要创建一个StringBuilder对象,而字符串拼接则需要创建多个StringBuilder和String对象
* ②、使用String的字符串拼接方式,内存中创建了较多的StringBuilder和String对象,内存占用更大,如垃圾回收效率要更频繁
* 优化:在基本确定要添加的字符串的长度不高于某个限定值highlevel,可以使用构造器new StringBuilder(参数)
*/
long start = System.currentTimeMillis();
method1(10000);
long end = System.currentTimeMillis();
System.out.println("method1花费时间为:" + (end - start));//89
long start1 = System.currentTimeMillis();
method2(10000);
long end1 = System.currentTimeMillis();
System.out.println("method2花费时间为:" + (end1 - start1));//0
}
public void method1(int highLevel){
String str = "";
for (int i = 0; i < highLevel; i++) {
str = str + "a";
}
}
public void method2(int highLevel){
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < highLevel; i++) {
stringBuilder = stringBuilder.append("a");
}
}
如果不是双引号声明的String对象,可以使用String提供的intern():intern方法会从字符串常量池中查询当前字符串是否存在,如不存在就会将当前字符串放入常量池中。
public class StringTest07 {
//以jdk8为例
public static void main(String[] args) {
String s = new String("1");
s.intern();//调用此方法之前,常量池中已经有“1"
String s1 = "1";
System.out.println(s == s1);//false
//s3变量的记录地址为new String(”11“),但在常量池中没有创建的”11“
String s3 = new String("1") + new String("1");
s3.intern();//该方法执行完就生成11,但没有在常量池中创建”11“,而是创建一个指向堆空间中new String(”11“)的地址
String s4 = "11";//使用的是上行代码生成的”11“ --》即常量池中生成的”11“的地址
System.out.println(s3 == s4);//true
}
}
总结:在jdk6中,如果串池中有,就返回已有的串池中的对象的地址,如果没有,就是将这个对象复制一份,放入串池,并返回这个对象的地址。从jdk7起,如果串池中有,就返回已有的串池中的对象的地址,如果没有,就是将这个对象的引用地址复制一份,放入串池,并返回串池中的引用地址。
参考代码:
public class StringTest08 {
static final int MAX_COUNT = 1000 * 10000;
static final String[] arr = new String[MAX_COUNT];
public static void main(String[] args) {
int[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9};
long start = System.currentTimeMillis();
for (int i = 0; i < MAX_COUNT; i++) {
//arr[i] = new String(String.valueOf(data[i % data.length]));
arr[i] = new String(String.valueOf(data[i % data.length])).intern();
}
long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start));
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.gc();
}
}
结论:对于程序中大量存在的字符串,尤其其中存在很多重复字符串时,使用intern()可以节省很多内存空间。
参考代码:
参数设置:-Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
public class StringTest09 {
public static void main(String[] args) {
for (int i = 0; i < 1000000; i++) {
String.valueOf(i).intern();
}
}
}