深入探究,String字符串性能优化,原来还可以这样

一.背景

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

如果不正确对待 String 对象,则可能导致一些问题的发生,比如因为使用了正则表达式对字符串进行匹配,从而导致并发瓶颈。

接下来我们就从 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语言中,Sun 公司的工程师们对String对象做了大量的优化,来节约内存空间,提升 String 对象在系统中的性能。如下图:

深入探究,String字符串性能优化,原来还可以这样

1.在 Java6 以及之前的版本中,String 对象是对 char 数组进行了封装实现的对象,主要有4个成员变量: char 数组、偏移量 offset、字符数量 count、哈希值 hash。

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

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

3.从 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修饰了。我们知道类被 final 修饰代表该类不可继承,而 char[] 被 final+private 修饰,代表了 String 对象不可被更改。Java实现的这个特性叫作 String 对象的不可变性,即 String 对象一旦创建成功,就不能再对它进行改变。

Java 这样做的好处在哪里呢?

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

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

3)可以实现字符串常量池。在 Java 中,通常有两种创建字符串对象的方式,一种是通过字符串常量的方式创建,如 String str = “abc”;另一种是字符串变量通过 new 形式的创建,如 String str = new String(“abc”)。

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

String str = new String(“abc”)这种方式,首先在编译类文件时,“abc”常量字符串将会放入到常量结构中,在类加载时,“abc”将会在常量池中创建;其次,在调用 new 时,JVM 命令将会调用 String 的构造函数,同时引用常量池中的 “abc” 字符串,在堆内存中创建一个 String 对象;最后, str 将引用 String 对象。

说到这里,将讲述一个特殊例子:平常编程时,对一个 String 对象 str 赋值 ”hello“,然后又让 str 赋值为 ”world“,这个时候 str 的值变成了 ”world“,那么 str 值确实改变了,为什么还说 String 对象不可变呢?

在这里要说明对象和对象引用的区别,在 Java 中要比较两个对象是否相等,往往要用 == ,而要判断两个对象的值是否相等,则需要用 equals 方法来判断。

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

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

四.String对象的优化

1.如何构建超大字符串?

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

String str = "ab" + "cd" + "ef";

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

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

String str = "abcdef";

上面讲的是字符串常量的累计,下面看字符串变量的累计:

  String str = "abcdef";

  for(int i = 0; i < 100; i++){

    str = str + i;
  }

上面的代码编译后,可以看到编译器同样对这段代码进行了优化,Java 在进行字符串的拼接时,偏向使用 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 对象的存储问题。先看下面一个案例:

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

public class Location{
    private String city;
    private String region ;
    private String countryCode;
    private double longitude;
    private double latitude;
}

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

public class ShareLocation{
    private String city;
    private String region ;
    private String countryCode;
}
public class Location{
    private ShareLocation shareLocation;
    private double longitude;
    private double latitude;
}

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

这是可以通过使用 String.intern 来节省内存空间,从而优化 String 对象的存储。

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

ShareLocation shareLocation = new ShareLocation();
shareLocation.setCity(messageInfo.getCity().intern());
shareLocation.setRegion(messageInfo.getRegion().intern());
shareLocation.setCountryCode(messageInfo.getCountryCode().intern()):

Location location = new Location();
location.set(shareLocation);
location.set(messageInfo.getLongitude());
location.set(messageInfo.getLatitude());

为了更好的理解,下面讲述一个简单的例子:

String a = new String("abc").intern();
String b = new String("abc").intern();
if(a == b){
    System.out.println("a == b");
}

运行结果: a == b

在字符串常量池中,默认会将对象放入常量池;在字符串变量中,对象是会在堆中创建,同时也会在常量池中创建一个字符串对象,String 对象中的 char 数组将会引用常量池中的 char 数组,并返回堆内存对象引用。

如果调用 intern 方法,会去查看字符串常量池中是否有等于该对象的字符串的引用,如果没有,在 JDK1.6 版本中去复制堆中的字符串到常量池中,并返回该字符串引用,堆内存中原有的字符串由于没有引用指向它,将会通过垃圾回收器回收。

在 JDK1.7 版本以后,由于常量池合并到了堆中,所以不会再复制具体字符串了,只是会把首次遇到的字符串的引用添加到常量池中;如果有,就返回常量池的字符串引用。

现在再来看上面的例子,在一开始字符串 “abc” 会在加载类时,在常量池中创建一个字符串对象。

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

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

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

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

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

深入探究,String字符串性能优化,原来还可以这样

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

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

Split() 方法使用了正则表达式实现了其强大的分割功能,而正则表达式的性能是非常不稳定的,使用不恰当会引起回溯问题,很可能导致 CPU 高居不下。

所以应该慎重使用 split() 方法,可以用 String.indexOf() 方法代替 split() 方法完成字符串的分割。如果实在无法满足需求,在使用 split() 方法时,对回溯问题需要加以重视。

五.总结

1)做好 String 字符串性能优化,可以提高系统的整体性能。在这个理论基础上,Java 版本在迭代中通过不断地更改成员变量,节约内存空间,对 String 对象优化。

2)String 对象的不可变性的特性实现了字符串常量池,通过减少同一个值的字符串对象的重复创建,进一步节约内存。

也是因为这个特性,我们在做长字符串拼接时,需要显示使用 StringBuilder,以提高字符串的拼接性能。

3)使用 intern 方法,让变量字符串对象重复使用常量池中相同值的对象,进而节约内存。

笔者福利

以下是小编自己针对马上即将到来的金九银十准备的一套“面试宝典”,不管是技术还是HR的问题都有针对性的回答。

有了这个,面试踩雷?不存在的!

需要这套“面试宝典”的,点击这里即可免费获取!回馈粉丝,诚意满满!!!




!**

需要这套“面试宝典”的,点击这里即可免费获取!回馈粉丝,诚意满满!!!

[外链图片转存中…(img-wTvdFaFZ-1623622436796)]
[外链图片转存中…(img-S10Z33CA-1623622436797)]
[外链图片转存中…(img-B0KRngs3-1623622436798)]

热门文章

暂无图片
编程学习 ·

那些年让我们目瞪口呆的bug

程序员一生与bug奋战&#xff0c;可谓是杀敌无数&#xff0c;见怪不怪了&#xff01;在某知识社交平台中&#xff0c;一个“有哪些让程序员目瞪口呆的bug”的话题引来了6700多万的阅读&#xff0c;可见程序员们对一个话题的敏感度有多高。 1、麻省理工“只能发500英里的邮件” …
暂无图片
编程学习 ·

redis的下载与安装

下载redis wget http://download.redis.io/releases/redis-5.0.0.tar.gz解压redis tar -zxvf redis-5.0.0.tar.gz编译 make安装 make install快链方便进入redis ln -s redis-5.0.0 redis
暂无图片
编程学习 ·

《大话数据结构》第三章学习笔记--线性表(一)

线性表的定义 线性表&#xff1a;零个或多个数据元素的有限序列。 线性表元素的个数n定义为线性表的长度。n为0时&#xff0c;为空表。 在比较复杂的线性表中&#xff0c;一个数据元素可以由若干个数据项组成。 线性表的存储结构 顺序存储结构 可以用C语言中的一维数组来…
暂无图片
编程学习 ·

对象的扩展

文章目录对象的扩展属性的简洁表示法属性名表达式方法的name属性属性的可枚举性和遍历可枚举性属性的遍历super关键字对象的扩展运算符解构赋值扩展运算符AggregateError错误对象对象的扩展 属性的简洁表示法 const foo bar; const baz {foo}; baz // {foo: "bar"…
暂无图片
编程学习 ·

让程序员最头疼的5种编程语言

世界上的编程语言&#xff0c;按照其应用领域&#xff0c;可以粗略地分成三类。 有的语言是多面手&#xff0c;在很多不同的领域都能派上用场。大家学过的编程语言很多都属于这一类&#xff0c;比如说 C&#xff0c;Java&#xff0c; Python。 有的语言专注于某一特定的领域&…
暂无图片
编程学习 ·

写论文注意事项

参考链接 给研究生修改了一篇论文后&#xff0c;该985博导几近崩溃…… 重点分析 摘要与结论几乎重合 这一条是我见过研究生论文中最常出现的事情&#xff0c;很多情况下&#xff0c;他们论文中摘要部分与结论部分重复率超过70%。对于摘要而言&#xff0c;首先要用一小句话引…
暂无图片
编程学习 ·

安卓 串口开发

上图&#xff1a; 上码&#xff1a; 在APP grable添加 // 串口 需要配合在项目build.gradle中的repositories添加 maven {url "https://jitpack.io" }implementation com.github.licheedev.Android-SerialPort-API:serialport:1.0.1implementation com.jakewhart…
暂无图片
编程学习 ·

2021-2027年中国铪市场调研与发展趋势分析报告

2021-2027年中国铪市场调研与发展趋势分析报告 本报告研究中国市场铪的生产、消费及进出口情况&#xff0c;重点关注在中国市场扮演重要角色的全球及本土铪生产商&#xff0c;呈现这些厂商在中国市场的铪销量、收入、价格、毛利率、市场份额等关键指标。此外&#xff0c;针对…
暂无图片
编程学习 ·

Aggressive cows题目翻译

描述&#xff1a; Farmer John has built a new long barn, with N (2 < N < 100,000) stalls.&#xff08;John农民已经新建了一个长畜棚带有N&#xff08;2<N<100000&#xff09;个牛棚&#xff09; The stalls are located along a straight line at positions…
暂无图片
编程学习 ·

剖析组建PMO的6个大坑︱PMO深度实践

随着事业环境因素的不断纷繁演进&#xff0c;项目时代正在悄悄来临。设立项目经理转岗、要求PMP等项目管理证书已是基操&#xff0c;越来越多的组织开始组建PMO团队&#xff0c;大有曾经公司纷纷建造中台的气质&#xff08;当然两者的本质并不相同&#xff0c;只是说明这个趋势…
暂无图片
编程学习 ·

Flowable入门系列文章118 - 进程实例 07

1、获取流程实例的变量 GET运行时/进程实例/ {processInstanceId} /变量/ {变量名} 表1.获取流程实例的变量 - URL参数 参数需要值描述processInstanceId是串将流程实例的id添加到变量中。变量名是串要获取的变量的名称。 表2.获取流程实例的变量 - 响应代码 响应码描述200指…
暂无图片
编程学习 ·

微信每天自动给女[男]朋友发早安和土味情话

微信通知&#xff0c;每天给女朋友发早安、情话、诗句、天气信息等~ 前言 之前逛GitHub的时候发现了一个自动签到的小工具&#xff0c;b站、掘金等都可以&#xff0c;我看了下源码发现也是很简洁&#xff0c;也尝试用了一下&#xff0c;配置也都很简单&#xff0c;主要是他有一…
暂无图片
编程学习 ·

C语言二分查找详解

二分查找是一种知名度很高的查找算法&#xff0c;在对有序数列进行查找时效率远高于传统的顺序查找。 下面这张动图对比了二者的效率差距。 二分查找的基本思想就是通过把目标数和当前数列的中间数进行比较&#xff0c;从而确定目标数是在中间数的左边还是右边&#xff0c;将查…
暂无图片
编程学习 ·

项目经理,你有什么优势吗?

大侠被一个问题问住了&#xff1a;你和别人比&#xff0c;你的优势是什么呢? 大侠听到这个问题后&#xff0c;脱口而出道&#xff1a;“项目管理能力和经验啊。” 听者抬头看了一下大侠&#xff0c;显然听者对大侠的这个回答不是很满意&#xff0c;但也没有继续追问。 大侠回家…
暂无图片
编程学习 ·

nginx的负载均衡和故障转移

#注&#xff1a;proxy_temp_path和proxy_cache_path指定的路径必须在同一分区 proxy_temp_path /data0/proxy_temp_dir; #设置Web缓存区名称为cache_one&#xff0c;内存缓存空间大小为200MB&#xff0c;1天没有被访问的内容自动清除&#xff0c;硬盘缓存空间大小为30GB。 pro…
暂无图片
编程学习 ·

业务逻辑漏洞

身份认证安全 绕过身份认证的几种方法 暴力破解 测试方法∶在没有验证码限制或者一次验证码可以多次使用的地方&#xff0c;可以分为以下几种情况︰ (1)爆破用户名。当输入的用户名不存在时&#xff0c;会显示请输入正确用户名&#xff0c;或者用户名不存在 (2)已知用户名。…