字符串性能优化
String对象是我们使用最频繁的一个对象类型,但它的性能问题却是最容易被忽略的。String对象作为Java语言中重要的数据类型,可以说是在内存中占据空间最大的一个对象。高效地使用字符串,可以提升系统的整体性能。
我们从String对象的实现、特性以及实际使用中的优化这三个方面入手,深入了解。
先看如下代码,创建3个对象,依次两两匹配,每组的结果是否相等?
String str1 = "abc";
String str2 = new String("abc");
String str3 = str2.intern();
asserSame(str1 == str2);
asserSame(str2 == str3);
asserSame(str1 == str3);
String对象是如何实现的?
在Java语言中,对String对象做了大量优化,来节约内存空间,提升String对象在系统中的性能,优化过程如图所示:
image.png
- 在Java6及之前版本中,String对象是对char数组进行封装实现的对象,主要有四个成员变量:char数组、偏移量offset、字符数量count、哈希值hash。String对象通过offset和count 两个属性来定位char[]数组,获取字符串。这样做可以高效、快速地共享数组对象,同时节省内存空间,但这种方式很有可能会导致内存泄漏。
- Java7版本和Java8版本,Java对String类做了一些改变。String类中不再有offset和count两个变量了。这样做的好处是String对象占用的内存减少了,同时String.substring()方法不再共享原对象的char[],从而解决了使用该方法可能导致的内存泄露问题。
- 从Java9开始,Java将char[]改为了byte[]字段,维护了新的属性coder,它是一个编码格式的标识。我们知道一个char字符占16位,2个字节。这个情况下,存储单字节编码内的字符就显得非常浪费。JDK9的String类为了节约内存空间,使用了占8位,一个字节的byte数组来存放字符串。新属性coder的作用是,在计算字符串长度或者使用indexOf()函数时,需要根据这个字段,判断如何计算字符串长度。coder属性默认0或1,0代表Latin-1单字节,1代表UTF-16。如果String判断字符串只含有Latin-1则coder等于0否则等于1。
String对象的不可变性
观察源码可以知道,String类是被final关键字修饰的,并且变量char[]也被final修饰了。我们知道被final修饰的类不可继承,变量被final+private修饰就不可更改。Java实现的这个特性叫做String对象的不可变性,即String对象一旦创建成功,就不能修改。这样做有什么好处呢?
- 保证了String对象的安全性。保证不会被恶意修改。
- 保证hash属性值不会频繁变更,使得类似HashMap容器能实现key-value缓存。
- 能够实现字符串常量池,当代码使用String str = "abc"; 创建对象时,JVM首先会检查该对象是否在字符串常量池中,如果在,则返回其引用,否则在常量池中被创建,这种实现可以减少相同对象的重复创建,节约内存。当使用String str = new String("abc");创建对象时,首先在编译类文件时,会将"abc"常量放到常量结构中,在类加载时,"abc"会在常量池中创建;其次,在调用new时,JVM将会调用String的构造函数,同时引用常量池的"abc",在堆中创建一个String对象;最后str会引用String对象。
而我们平时的使用中会发现,String str="abc";str="bcd";这样的语句,这里str是可变的。其实,这里的str只是对String对象的引用,原来的对象仍旧存在于内存中。
String对象的优化
接下来我们根据String对象的特性,看看如何优化String对象,优化的过程中有什么需要注意的地方。
-
构建超大字符串
String str = "a"+"b"+"c";
对于上面的代码,我们知道,JVM首先会生成a、b、c三个对象,最后生成abc对象,理论上来讲这样的代码效率会很低。但在实际运行中,我们就会发现,编译器自动将这条语句优化为
String str = "abc";
上述代码是字符串常量的累加,那么对于字符串变量,编译器是否会进行同样的优化呢?
对于String str="abc"; for(int i=0;i<100;i++){ str+=i; }
这段代码,编译器同样会进行优化,优化的结果是这样的
String str="abc"; for(int i=0;i<100;i++){ str=(new StringBuilder(String.valueOf(str))).append(i).toString(); }
综上,即使使用+进行字符串拼接,也同样会被编译器优化为StringBuilder方式,但我们发现,编译器的优化,每次循环就会创建一个新的StringBuilder对象,同样会降低系统性能。所以,平时进行字符串拼接的时候,建议显式使用StringBuilder提升系统性能。在多线程编程中String对象的拼接涉及到线程安全,我们可以使用StringBuffer,但是StringBuffer涉及到锁竞争,所以从性能上来说,要比StringBuilder差一些。
-
使用String.intern节省内存,每次赋值时使用String的intern方法,可以大幅度降低重复信息的内存占用率。调用intern方法,JVM回去检查字符串常量池中是否有等于该对象的字符串的引用,如果没有,在JDK1.6中会复制堆内存中的字符串到常量池中,并返回引用,堆内存中的字符串会通过垃圾回收器回收。在JDK1.7后,常量池合并到堆中,不需要再复制字符串,只会把首次遇到的字符串的引用添加到常量池中;如果有,就返回引用。
image.png使用intern方法需要注意,一定要结合场景。常量池是类似HashTable的实现,存储的数据越大,遍历的时间复杂度越大,数据如果过大,会增大字符串常量池的负担。
-
谨慎选择字符串分割,Split()作为分割字符串的方法,其内部是使用正则表达式来实现的,会出现回溯的风险,建议使用indexOf方法代替Split方法完成分割。如果一定要使用Split方法,就需要对回溯问题加以重视。
网友评论