String字符串性能优化的探究

一.背景

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

  若是不正确对待 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 对象在系统中的性能。以下图:                                                                                              性能优化

  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 字符串的建立分配内存地址状况:                                                                                                                           

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

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

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

  因此应该慎重使用 split() 方法,能够用 String.indexOf() 方法代替 split() 方法完成字符串的分割。若是实在没法知足需求,在使用 split() 方法时,对回溯问题须要加以重视。

五.总结

  1)作好 String 字符串性能优化,能够提升系统的总体性能。在这个理论基础上,Java 版本在迭代中经过不断地更改为员变量,节约内存空间,对 String 对象优化。

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

        也是由于这个特性,咱们在作长字符串拼接时,须要显示使用 StringBuilder,以提升字符串的拼接性能。

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