String作为我们使用最频繁的一种对象类型,其性能问题是最容易被忽略的。作为Java中重要的数据类型,是内存中占据空间比较大的一个对象。如何高效地使用字符串,可以帮助我们提升系统的整体性能。
现在,我们就从String对象的实现、特性以及实际使用中的优化这几方面来入手,深入理解以下String的性能优化。
在这之前,首先看一个问题。通过三种方式创建三个对象,然后依次两两匹配,得出的结果是什么?答案留到最后揭晓。
String str1 = "abc"; String str2 = new String("abc"); String str3 = str2.intern(); System.out.println(str1 == str2); System.out.println(str2 == str3); System.out.println(str1 == str3);
String对象是如何实现的?
Java中对String对象做了大量的优化,以此来节约内存空间,提升String对象的性能。下图是Java6 -> Java9 String对象属性的变化:
可以看到,String的属性有了以下的变化:
- 在Java6及以前的版本中,String对象是对char数组进行了封装实现的对象,主要有:char数组、偏移量offset、字符数量count、哈希值hash。String对象通过offset和count属性来定位char数组,获取字符串。这样做可以高效快速地共享数组对象,能节省内存空间,但容易出现内存泄漏。
- 从Java7到Java8版本,Java对String做了一些改变。String类中不再有offset和count两个属性了。这样做可以使String对象占用的内存减少,并且String.substring方法也不再共享char[],解决了可能出现的内存泄漏的问题。
- 从Java9版本开始,将char[]改成了byte[],并增加了新属性coder,coder是一个编码格式的标识。
为什么要这么改呢?
我们知道,一个char字符占16位,2个字节。这种情况下存储单字节的字符就很容易浪费了。JDK1.9的String类为了节省内存空间,就使用了占8位,1个字节的byte数组来存储字符串。
coder属性的作用是:在计算字符串长度或者使用indexOf()时,需要根据这个字段,判断如何计算字符串的长度。coder属性值默认有0和1两个值,0代表Latin-1(单字节编码),1代表UTF-16。如果String判断字符串只包含Latin-1,则coder值取0,反之为1。
String对象的不可变性
如果看过String的源码,就会发现,String类是被final关键字修饰的,且变量char数组也被final修饰。
一个类被final修饰代表着该类不可继承,char[]被private和final修饰着,代表String对象不可被更改。这就叫做String对象的不可变性。即如果String对象一旦创建成功了,就不能再对它进行改变。
这样做的好处在哪里?
第一、保证了String对象的安全性。假设String对象是可变的,那么String对象就会被恶意修改。
第二.、保证hash属性值不会频繁变更,确保了唯一性。使得类似HashMap容器才能实现相应的key-value缓存功能。
第三、可以实现字符串常量池。Java中,通常有2种创建字符串对象的方式,一种是通过字符串常量的方式创建,如String str = "abc";另一种是字符串常量通过new形式的创建,如String str = new String("abc")。
当代码中使用第一种方式创建字符串对象时,JVM首先检查该对象是否在字符串常量池中,如果在就返回该对象的引用,否则新的字符串将在常量池中创建。这种方式可以减少同一个值的字符串对象的重复创建,节约内存。
第二种方式,首先在编译类文件时,"abc"常量字符串将会放入到常量结构中,在类加载时,"abc"会在常量池中创建;然后调用new时,JVM命令将会调用String的构造函数,同时引用常量池的"abc"字符串,在堆内存中创建一个String对象,最后str引用String对象。
String对象的优化
1.如何构建超大字符串
编程过程中字符串的拼接很常见。如果使用String对象相加,拼接我们想要的字符串,会不会产生多个对象呢?比如说以下代码:
String str = "ab" + "cd" + "ef";
分析代码可知:首先会生成ab对象,再生成abcd对象,最后生成abcdef对象。理论上说,代码很低效。
但实际上,会发现只有一个对象生成,这是为什么呢?编译时编译器会自动帮我们优化代码,使得最后只得出一个对象“abcdef”。
再来看看,如果进行字符串常量的累计,又会出现什么结果?
String str = "abcdef"; for (int i = 0; i < 100; i++) { str = str + i; }
上面的代码编译后,编译器同样对代码进行了优化,在进行字符串拼接时,偏向使用StringBuilder,这样可以提升效率。上面的代码变成了下面这样:
String str = "abcdef"; for (int i = 0; i < 100; i++) { str = (new StringBuilder(String.valueOf(str))).append(i).toString(); }
总结:即使使用+号作为字符串的拼接,一样可以被编译器优化成StringBuilder的方式。但如果每次循环都生成一个新的StringBuilder实例,同样会降低系统的性能。所以平时做字符串拼接的时候,建议还是显示使用StringBuilder来提升性能。在多线程编程时,String对象的拼接涉及到了线程安全,可以使用StringBuffer。但由于StringBuffer是线程安全的,涉及到锁竞争,所以就性能上来说会比StringBuilder差些。
2.如何使用String.intern节省内存?
对于一些数据,数据量非常大,但同时又有大部分重合的,该如何处理呢?
具体做法是,每次赋值的时候使用String的intern方法,如果常量池中有相同值,就会重复使用该对象,返回对象的引用,这样一开始的对象就可以被回收掉了,这样的话数据量就会大幅度降低了。
我们再来看一个例子:
String a = new String("abc").intern(); String b = new String("abc").intern(); if (a == b) { System.out.println("a == b"); }
输出结果是: a == b
在字符串常量池中,默认会将对象放入常量池;在字符串变量中,对象总是创建在堆内存的,同时也会在常量池中创建一个字符串对象,复制到堆内存对象中,并返回堆内存对象引用。
如果调用intern方法,会去查看字符串常量池中是否有等于该对象的字符串,如果没有,就会在常量池中新增该对象,并返回该对象引用;如果有则返回常量池中的字符串引用。堆内存中原有的对象由于没有引用指向它,将会通过垃圾回收器回收。
3.如何使用字符串的分割方法?
spilt()方法使用了正则表达式实现了强大的分割功能,而正则表达式的性能是非常不稳定的,使用不当会引起回溯问题,很可能导致CPU居高不下。
所以要慎重使用spilt方法,我们可以用String.indexOf()方法代替spilt()方法完成字符串的分割,如果实在无法满足需求,就在使用spilt方法时,对回溯问题加以重视就可以了。
总结
通过上面的叙述,我们认识到了做好String字符串性能的优化,可以提升整个系统的性能。在这个理论基础上,Java版本在迭代中不断更改成员变量,节约内存空间,对String性能进行了优化。
我们还提到了String对象的不可变性,正是这个特性实现了字符串常量池,通过减少同一个值的字符串对象的重复创建,进一步节约内存。也是因为这个特性,我们在做长字符串的拼接时,需要显示使用StringBuilder,以提升字符串的拼接性能。最后在优化方面,我们还可以使用intern方法,让变量字符串对象重复使用常量池中相同值的对象,进而节约内存。
最后,公布上面那道题的结果:
false、false、true。
其中, String str1 = “abc”;通过字面量的方式创建,abc存储于字符串常量池中;
String str2 = new String("abc");通过new对象的方式创建字符串对象,引用地址存放在堆内存中,abc则存放在字符串常量池中,所以为false;
String str3 = str2.intern();由于调用了intern()方法,会返回常量池中的数据,str3此时就指向常量池中的abc,和str1的方式一样,所以为true;
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持自由互联。