字符串性能优化不容小觑

字符串性能优化不容小觑

概述

​ String对象是我们使用最频繁的一个对象类型,但它的性能问题却是最容易被忽略的。String对象作为Java语言中重要的数据类型,是内存中占据空间最大的一个对象。高效地使用字符串,可以提升系统的整体性能。

程序案例

Stringstr1="abc";
Stringstr2=newString("abc");
Stringstr3=str2.intern();
System.out.println(str1==str2);	//false
System.out.println(str2==str3);	//false
System.out.println(str1==str3);//true

String如何实现

​ 在Java语言中,Sun公司的工程师们对String对象做了大量的优化,来节约内存空间,提升String对象在系统中的性能。一起来看看优化过程,如下图所示:

在这里插入图片描述

Java6以及之前的版本

​ String对象是对char数组进行了封装实现的对象,主要有四个成员变量:char数组偏移量offset字符数量count哈希值hash

​ String对象是通过offset和count两个属性来定位char[]数组,获取字符串。这么做可以高效、快速地共享数组对象,同时节省内存空间(subString共享char[]),但这种方式很有可能会导致内存泄漏

从Java7版本开始到Java8版本

​ Java对String类做了一些改变。String类中不再有offset和count两个变量了。这样的好处是String对象占用的内存稍微少了些,同时,String.substring方法也不再共享char[],从而解决了使用该方法可能导致的内存泄漏问题。

从Java9版本开始

​ 工程师将char[]字段改为了byte[]字段,又维护了一个新的属性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类被final关键字修饰了,而且变量char数组也被final修饰了。我们知道String类被final修饰代表该类不可继承,而char[]被final+private修饰,代表了String对象不可被更改。Java实现的这个特性叫作String对象的不可变性,即String对象一旦创建成功,就不能再对它进行改变。

好处

  • 保证String对象的安全性。假设String对象是可变的,那么String对象将可能被恶意修改。

  • 保证hash属性值不会频繁变更,确保了唯一性,使得类似HashMap容器才能实现相应的key-value缓存功能。

  • 可以实现字符串常量池。在Java中,通常有两种创建字符串对象的方式,

    • 一种是通过字符串常量的方式创建,如Stringstr=“abc”;

    • 另一种是字符串变量通过new形式的创建,如Stringstr=newString(“abc”)。

      当代码中使用第一种方式创建字符串对象时,JVM首先会检查该对象是否在字符串常量池中,如果在,就返回该对象引用,否则新的字符串将在常量池中被创建。这种方式可以减少同一个值的字符串对象的重复创建,节约内存。

反例

​ 平常编程时,对一个String对象str赋值“hello”,然后又让str值为“world”,这个时候str的值变成了“world”。那么str值确实改变了,为什么我还说String对象不可变呢?

​ 首先,我来解释下什么是对象和对象引用。Java初学者往往对此存在误区,特别是一些从PHP转Java的同学。在Java中要比较两个对象是否相等,往往是用==,而要判断两个对象的值是否相等,则需要用equals方法来判断。

​ 这是因为str只是String对象的引用,并不是对象本身。对象在内存中是一块内存地址,str则是一个指向该内存地址的引用。所以在刚刚我们说的这个例子中,第一次赋值的时候,创建了一个“hello”对象,str引用指向“hello”地址;第二次赋值的时候,又重新创建了一个对象“world”,str引用指向了“world”,但hello”对象依然存在于内存中。

​ 也就是说str并不是对象,而只是一个对象引用。真正的对象依然还在内存中,没有被改变。

String对象的优化

​ 了解了String对象的实现原理和特性,接下来我们就结合实际场景,看看如何优化String对象的使用,优化的过程中又有哪些需要注意的地方。

如何构建超大字符串

​ 编程过程中,字符串的拼接很常见。前面我讲过String对象是不可变的,如果我们使用String对象相加,拼接我们想要的字符串,是不是就会产生多个对象呢?例如以下代码:

Stringstr="ab"+"cd"+"ef";

​ 分析代码可知:首先会生成ab对象,再生成abcd对象,最后生成abcdef对象,从理论上来说,这段代码是低效的。

​ 但实际运行中,我们发现只有一个对象生成,这是为什么呢?难道我们的理论判断错了?我们再来看编译后的代码,你会发现编译器自动优化了这行代码,如下:

Stringstr="abcdef";

​ 上面我介绍的是字符串常量的累计,我们再来看看字符串变量的累计又是怎样的呢?

Stringstr="abcdef";
for(inti=0;i<1000;i++){
str=str+i;
}

​ 上面的代码编译后,你可以看到编译器同样对这段代码进行了优化。不难发现,Java在进行字符串的拼接时,偏向使用StringBuilder,这样可以提高程序的效率。

下面是被jvm优化后的

Stringstr="abcdef";
for(inti=0;i<1000;i++){
str=(newStringBuilder(String.valueOf(str))).append(i).toString();
}

​ 综上已知:**即使使用+号作为字符串的拼接,也一样可以被编译器优化成StringBuilder的方式。但再细致些,你会发现在编译器优化的代码中,每次循环都会生成一个新的StringBuilder实例,同样也会降低系统的性能。所以平时做字符串拼接的时候,我建议你还是要显示地使用StringBuilder来提升系统性能。

​ 如果在多线程编程中,String对象的拼接涉及到线程安全,你可以使用StringBuffer。但是要注意,由于StringBuffer是线程安全的,涉及到锁竞争,所以从性能上来说,要比StringBuilder差一些

如何使用String.intern节省内存

​ 讲完了构建字符串,我们再来讨论下String对象的存储问题,先看一个案例。

​ Twitter每次发布消息状态的时候,都会产生一个地址信息,以当时Twitter用户的规模预估,服务器需要32G的内存来存储地址信息。

publicclassLocation{
    privateStringcity;
    privateStringregion;
    privateStringcountryCode;
    privatedoublelongitude;
    privatedoublelatitude;
}

​ 考虑到其中有很多用户在地址信息上是有重合的,比如,国家、省份、城市等,这时就可以将这部分信息单独列出一个类,以减少重复,代码如下:

publicclassSharedLocation{	
    privateStringcity;
    privateStringregion;
    privateStringcountryCode;
}

publicclassLocation{	
    privateSharedLocationsharedLocation;
    doublelongitude;doublelatitude;
}

​ 通过优化,数据存储大小减到了20G左右。但对于内存存储这个数据来说,依然很大,怎么办呢?

​ 这个案例来自一位Twitter工程师在QCon全球软件开发大会上的演讲,他们想到的解决方法,就是使用String.intern来节省内存空间,从而优化String对象的存储。

​ 具体做法就是,在每次赋值的时候使用String的intern方法,如果常量池中有相同值,就会重复使用该对象,返回对象引用,这样一开始的对象就可以被回收掉。这种方式可以使重复性非常高的地址信息存储大小从20G降到几百兆。

SharedLocationsharedLocation=newSharedLocation();
sharedLocation.setCity(messageInfo.getCity().intern());		
sharedLocation.setCountryCode(messageInfo.getRegion().intern());
sharedLocation.setRegion(messageInfo.getCountryCode().intern());
Locationlocation=newLocation();
location.set(sharedLocation);
location.set(messageInfo.getLongitude());
location.set(messageInfo.getLatitude());

为了更好地理解,我们再来通过一个简单的例子,回顾下其中的原理:

Stringa=newString("abc").intern();
Stringb=newString("abc").intern();

if(a==b){
System.out.print("a==b");
}

输出结果:

a==b

​ 在字符串常量中,默认会将对象放入常量池;在字符串变量中,对象是会创建在堆内存中。

​ 如果调用intern方法,会去查看字符串常量池中是否有等于该对象的字符串,如果没有则在常量池中新增该对象,并返回该对象引用;如果有,则返回常量池中的字符串。堆内存中原有的对象由于没有引用指向它,将会通过垃圾回收器回收。

使用intern方法需要注意的一点是,一定要结合实际场景。因为常量池的实现是类似于一个HashTable的实现方式,HashTable存储的数据越大,遍历的时间复杂度就会增加。如果数据过大,会增加整个字符串常量池的负担。

如何使用字符串的分割方法

​ 最后我想跟你聊聊字符串的分割,这种方法在编码中也很最常见。Split()方法使用了正则表达式实现了其强大的分割功能,而正则表达式的性能是非常不稳定的,使用不恰当会引起回溯问题,很可能导致CPU居高不下。
所以我们应该慎重使用Split()方法,我们可以用String.indexOf()方法代替Split()方法完成字符串的分割。如果实在无法满足需求,你就在使用split()方法时,对回溯问题加以重视就可以了。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值