前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >字符串性能优化不容小觑

字符串性能优化不容小觑

作者头像
故里
发布2020-11-25 16:02:48
4930
发布2020-11-25 16:02:48
举报
文章被收录于专栏:故里学Java故里学Java

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

今天这篇文章我们从String对象的实现、特性以及实际使用中的优化三方面,来深入了解String对象。

String对象是如何实现的

在Java更新的版本变化中,对String对象已经做了大量的优化,来节约内存空间,提升String对象在系统中的性能。来看看在Java版本迭代中String的优化过程;

  • 在Java6以及以前的版本中,String对象是对char数组进行了封装实现的对象,主要有四个成员变量:char数组、偏移量offset、字符数量count、哈希值hash。
  • 在Java7和8版本中,Java对String类做了改变,不再有offset和count两个变量,这样可以稍微减少String对象占用的内存。同时,String.substring()不再共享char[],从而解决了使用该方法可能导致的内存泄露问题。
  • 从Java9版本开始,char[]改成了byte[],又维护了一个新的属性coder,它是一个编码格式的标识。

为什么从char[]改变成byte[],我们都知道一个char字符占用16位,2个字节,这种情况在存储单字节编码内的字符就有点浪费。Java9中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关键字修饰,而且遍历charp[]也被final修饰。类被final修饰代表String类不能被继承,而charp[]被private和final修饰,代表了String对象不可被更改。Java实现的这个特性叫做String对象的不可变性,即String对象一旦被创建成功就不能进行修改了。

String对象不可变性有哪些好处?

  1. 保证了String对象的安全性。如果 String 对象是可变的,那么 String 对象将可能被恶意修改。
  2. 保证了hash属性值不会频繁变更,确保了唯一性,使得类似 HashMap 容器才能实现相应的 key-value 缓存功能。
  3. 可以实现字符串常量池。Java中有两种创建字符串对象的方式,一种是通过字符串常量的方式创建,另外一种是字符串变量通过new的形式创建。

当我们使用字符串常量创建字符串对象时,JVM会先检查该对象是在字符串常量池中,如果在就返回该对象的引用,否则新创建一个字符串对象保存到字符串常量池,并使用这个引用。这种方式可以减少同一个值的字符串对象的重复创建,节约了内存空间。

当我们使用new的形式创建,比如String str = new String(“abc”),在编译类文件的时候,“abc”常量字符串将会放入常量结构中,在类加载时,“abc”将会放到常量池中创建,在调用new时,JVM命令将会调用String的构造函数,同时引用常量池中的“abc"字符串,在堆内存中创建一个String对象,最后 str引用String对象。

String对象的优化

上边我们了解了String对象实现原理和特性,下边将结合实际场景,看看String对象在我们实际使用中有哪些需要注意的地方。

1. 字符串拼接

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

代码语言:javascript
复制
String str = "ab" + "cd" + "ef";

分析可知,首先会生成ab对象,再生成abcd对象,最后生成abcdef对象,从理论上讲,这样做的效率很低。但是实际运行中,我们发现只有一个对象生成,这是为什么?我们再来看看编译后的代码,你会发现上边的代码编译器自动做了优化,如下:

代码语言:javascript
复制
String str = "abcdef";

上面介绍的是字符串常量的累加,再来看看字符串变量的累加:

代码语言:javascript
复制
String str = "abcdef";
for (int i = 0; i<1000;i++) {
  str = str + i;
}

编译后,我们可以看到编译器同样对这段代码进行了优化,Java在进行字符串拼接时,偏向于使用StringBuilder,这样可以提高程序的效率。

代码语言:javascript
复制
String str = "abcdef";
for(int i=0; i<1000; i++) {
            str = (new StringBuilder(String.valueOf(str))).append(i).toString();
}

我们可以看到即使使用‘’+”号作为字符串拼接,也一样被编译器优化成StringBuilder的方式,但是,仔细一看,你会发现编译器优化后的代码,每次循环的时候都会生成一个新的StringBuilder对象,同样会降低系统的性能。

我们平时做字符串拼接的时候,建议显示地使用StringBuilder来提升系统性能,如果是多线程编程中,String对象的拼接涉及到线程安全,可以使用StringBuffer。由于StringBuffer是线程安全的,涉及到锁竞争,所以从性能上说,要比StringBuilder差一些。

2. 使用String.intern节省内存

在每次赋值的时候使用 String 的 intern 方法,如果常量池中有相同值,就会重复使用该对象,返回对象引用,为了更好的理解,我们举一个简单的例子来看一下:

代码语言:javascript
复制
String a =new String("abc").intern();
String b = new String("abc").intern();
        
if(a==b) {
    System.out.print("a==b");
}

输出结果,a==b,分析一下;

创建 a 变量时,调用 new Sting() 会在堆内存中创建一个 String 对象,String 对象中的 char 数组将会引用常量池中字符串。在调用 intern 方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。

创建 b 变量时,调用 new Sting() 会在堆内存中创建一个 String 对象,String 对象中的 char 数组将会引用常量池中字符串。在调用 intern 方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。

而在堆内存中的两个对象,由于没有引用指向它,将会被垃圾回收。所以 a 和 b 引用的是同一个对象。

如果在运行时,创建字符串对象,将会直接在堆内存中创建,不会在常量池中创建。所以动态创建的字符串对象,调用 intern 方法,在 JDK1.6 版本中会去常量池中创建运行时常量以及返回字符串引用,在 JDK1.7 版本之后,会将堆中的字符串常量的引用放入到常量池中,当其它堆中的字符串对象通过 intern 方法获取字符串对象引用时,则会去常量池中判断是否有相同值的字符串的引用,此时有,则返回该常量池中字符串引用,跟之前的字符串指向同一地址的字符串对象。

用一张图来总结String字符串的创建和分配内存地址的情况:

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

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

分割字符串我们常用的方法就是Split()方法,Split()方法使用了正则表达式实现了其强大的分割能力,但是正则表达式的性能是很不稳定的,使用不当就会引起回溯问题,很有可能导致CPU居高不下。

在日常使用的时候,可以用String.indexOf()方法代替Split()方法完成对字符串的分割,如果无法满足需要,在使用Split()方法的时候对回溯问题要加以重视。

最后

以一个小问题结束本次分享,下边每组匹配的两个对象是否相等?

代码语言:javascript
复制
String str1= "abc";
String str2= new String("abc");
String str3= str2.intern();
assertSame(str1==str2);
assertSame(str2==str3);
assertSame(str1==str3)

如果对你有帮助,不妨关注一下吧

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-11-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 故里学Java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • String对象是如何实现的
  • String对象的不可变性
  • String对象的优化
    • 1. 字符串拼接
      • 2. 使用String.intern节省内存
        • 3. 如何使用字符串的分割方法
        • 最后
        相关产品与服务
        容器服务
        腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档