1、引言git
String 对象是咱们使用最频繁的一个对象类型,但它的性能问题倒是最容易被忽略的。String 对象做为 Java 语言中重要的数据类型,是内存中占据空间最大的一个对象。高效地使用字符串,能够提高系统的总体性能。正则表达式
2、String 对象的实现编程
在 Java 语言中,Sun 公司的工程师们对 String 对象作了大量的优化,来节约内存空间,提高 String 对象在系统中的性能。数组
1. 在 Java6 以及以前的版本中,String 对象是对 char 数组进行了封装实现的对象,主要有四个成员变量: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。并发
3、String 对象的不可变性原由
了解了 String 对象的实现后,你有没有发如今实现代码中 String 类被 final 关键字修饰了,并且变量 char 数组也被 final 修饰了。
咱们知道类被 final 修饰表明该类不可继承,而 char[] 被 final+private 修饰,表明了 String 对象不可被更改。Java 实现的这个特性叫做 String 对象的不可变性,即 String 对象一旦建立成功,就不能再对它进行改变。
4、String 对象的不可变性好处
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 对象。
5、String 对象的优化
1. 如何构建超大字符串?
编程过程当中,字符串的拼接很常见。前面我讲过 String 对象是不可变的,若是咱们使用 String 对象相加,拼接咱们想要的字符串,是否是就会产生多个对象呢?例如如下代码:
String str= "ab" + "cd" + "ef";
分析代码可知:首先会生成 ab 对象,再生成 abcd 对象,最后生成 abcdef 对象,从理论上来讲,这段代码是低效的。
但实际运行中,咱们发现只有一个对象生成,这是为何呢?难道咱们的理论判断错了?咱们再来看编译后的代码,你会发现编译器自动优化了这行代码,以下:
String str= "abcdef";
上面介绍的是字符串常量的累计,再来看看字符串变量的累计又是怎样的呢?
String str = "abcdef";
for(int i=0; i<1000; i++) {
str = str + i;
}
上面的代码编译后,你能够看到编译器一样对这段代码进行了优化。不难发现,Java 在进行字符串的拼接时,偏向使用 StringBuilder,这样能够提升程序的效率。
String str = "abcdef";
for(int i=0; i<1000; i++) {
str = (new StringBuilder(String.valueOf(str))).append(i).toString();
}
综上已知:即便使用 + 号做为字符串的拼接,也同样能够被编译器优化成 StringBuilder 的方式。但再细致些,你会发如今编译器优化的代码中,每次循环都会生成一个新的 StringBuilder 实例,一样也会下降系统的性能。
因此平时作字符串拼接的时候,我建议你仍是要显示地使用 String Builder 来提高系统性能。
若是在多线程编程中,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 SharedLocation {
private String city;
private String region;
private String countryCode;
}
public class Location {
private SharedLocation sharedLocation;
double longitude;
double latitude;
}
经过优化,数据存储大小减到了 20G 左右。但对于内存存储这个数据来讲,依然很大,怎么办呢?
这个案例来自一位 Twitter 工程师在 QCon 全球软件开发大会上的演讲,他们想到的解决方法,就是使用 String.intern 来节省内存空间,从而优化 String 对象的存储。
具体作法就是,在每次赋值的时候使用 String 的 intern 方法,若是常量池中有相同值,就会重复使用该对象,返回对象引用,这样一开始的对象就能够被回收掉。这种方式可使重复性很是高的地址信息存储大小从 20G 降到几百兆。
SharedLocation sharedLocation = new SharedLocation();
sharedLocation.setCity(messageInfo.getCity().intern()); sharedLocation.setCountryCode(messageInfo.getRegion().intern());
sharedLocation.setRegion(messageInfo.getCountryCode().intern());
Location location = new Location();
location.set(sharedLocation);
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.print("a==b");
}
输出结果:
a==b
在字符串常量中,默认会将对象放入常量池;在字符串变量中,对象是会建立在堆内存中,同时也会在常量池中建立一个字符串对象,复制到堆内存对象中,并返回堆内存对象引用。
若是调用 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() 方法时,对回溯问题加以重视就能够了。
6、总结
咱们认识到作好 String 字符串性能优化,能够提升系统的总体性能。在这个理论基础上,Java 版本在迭代中经过不断地更改为员变量,节约内存空间,对 String 对象进行优化。
咱们还特别提到了 String 对象的不可变性,正是这个特性实现了字符串常量池,经过减小同一个值的字符串对象的重复建立,进一步节约内存。
但也是由于这个特性,咱们在作长字符串拼接时,须要显示使用 StringBuilder,以提升字符串的拼接性能。最后,在优化方面,咱们还可使用 intern 方法,让变量字符串对象重复使用常量池中相同值的对象,进而节约内存。
最后再分享一个我的观点。那就是千里之堤,溃于蚁穴。平常编程中,咱们每每可能就是对一个小小的字符串了解不够深刻,使用不够恰当,从而引起线上事故。
好比,在我以前的工做经历中,就曾由于使用正则表达式对字符串进行匹配,致使并发瓶颈,这里也能够将其概括为字符串使用的性能问题。