本博客来自个人新书Java性能优化(暂定名),第二章的节选2.1和2.2,2.10. 也欢迎购买个人书 《Spring Boot 2 精髓 》
程序员
字符串在Java里是不可变的,不管是构造,仍是截取,获得的老是一个新字符串。看一下构造一个字符串源码spring
private final char value[]; public String(String original) { this.value = original.value; this.hash = original.hash; }
原有的字符串的value数组直接经过引用赋值给新的字符串value,也就是俩个字符串共享一个char数组,所以这种构造方法有着最快的构造。Java里的String对象被设计为不可变。意思是指一旦程序得到了字符串对象引用,没必要担忧这个字符串在别的地方被修改,不可变意味着线程安全,在第三章对不可变对象线程安全性又说明。数组
构造字符串更多的状况构造字符串是经过一个字符串数组,或者在某些框架的反序列化,使用byte[] 来构造字符串,这种状况下性能会很是低。 以下是经过char[]数组构造一个新的字符串源码安全
public String(char value[]) { this.value = Arrays.copyOf(value, value.length); }
Arrays.copyOf 会从新拷贝一份新的数组,方法以下springboot
public static char[] copyOf(char[] original, int newLength) { char[] copy = new char[newLength]; System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; }
能够看到经过数组构造字符串其实是会建立一个新的字符串数组。若是不这样,仍是直接引用char数组,那么外部若是更改char数组,则这个新的字符串就被改变了。性能优化
char[] cs = new char[]{'a','b'}; String str = new String(cs); cs[0] ='!'
上面的代码最后一行,修改了cs数组,但不会影响str。由于str其实是新的字符串数组构成服务器
经过char数组构造新的字符串是最长用的方法,咱们后面看到几乎每一个字符串API,都会调用这个方法构造新的字符串,好比subString,concat等方法。以下代码验证了经过字符串构造新的字符串,以及使用char数组构造字符串性能比较并发
String str= "你好,String"; char[] chars = str.toCharArray(); [@Benchmark](https://my.oschina.net/u/3268003) public String string(){ return new String(str); } [@Benchmark](https://my.oschina.net/u/3268003) public String stringByCharArray(){ return new String(chars); }
输出按照ns/op来输出,既每次调用所用的纳秒数,能够看到经过char构造字符串仍是先当耗时的,特别若是是数组特别长,那更加耗时app
Benchmark Mode Score Units c.i.c.c.NewStringTest.string avgt 4.235 ns/op c.i.c.c.NewStringTest.stringByCharArray avgt 11.704 ns/op
经过字节构造字符串,是一种很是常见的状况,尤为如今分布式和微服务流行,字符串在客户端序列化成字节数组,并发送给你给服务器端,服务器端会有一个反序列化,经过byte构造字符串框架
以下测试使用byte构造字符串性能测试
byte[] bs = "你好,String".getBytes("UTF-8"); [@Benchmark](https://my.oschina.net/u/3268003) public String stringByByteArray() throws Exception{ return new String(bs,"UTF-8"); }
测试结果能够看到byte构造字符串太耗时了,尤为是当要构造的字符串很是长的时候
Benchmark Mode Score Units c.i.c.c.NewStringTest.string avgt 4.649 ns/op c.i.c.c.NewStringTest.stringByByteArray avgt 82.166 ns/op c.i.c.c.NewStringTest.stringByCharArray avgt 12.138 ns/op
经过字节数组构造字符串,主要涉及到转码过程,内部会调用 StringCoding.decode转码
this.value = StringCoding.decode(charsetName, bytes, offset, length);
charsetName表示字符集,bytes是字节数组,offset和length表示字节数组
实际负责转码的是Charset子类,好比sun.nio.cs.UTF_8的decode方法负责实现字节转码,若是在深刻到这个类,你会发现,你看到的是冰上一角,冰上下面这是一个至关耗CPU计算转码的工做,属于没法优化的部分.
在我屡次的系统性能优化过程当中,都会发现经过字节数据组构造字符串老是排在消耗CPU比较靠前的位置,转码消耗的系统性能抵得上百行的业务代码。 所以咱们系统在设计到分布式的,须要仔细设计须要传输的字段,尽可能避免用String。好比时间能够用long类型来表示,业务状态也能够用int来表示。以下须要序列化的对象
public class OrderResponse{ //订单日期,格式'yyyy-MM-dd' private String createDate; //订单状态,"0"表示正常 private String status; }
能够改进成更好的定义,以减少序列化和反序列化负担。
public class OrderResponse{ //订单日期 private long createDate; //订单状态,0表示正常 private int status; }
关于在微服务中,序列化和反序列化传输对象,会在第四章和五章再次介绍对象的序列化
JDK会自动将使用+号作的字符串拼接自动转化为StringBuilder,以下代码:
String a="hello"; String b ="world " String str=a+b;
虚拟机会编译成以下代码
String str = new StringBuilder().append(a).append(b).toString();
若是你运行JMH测试这俩段代码,性能其实同样的,由于使用+链接字符串是一个常见操做,虚拟机对如上俩个代码片断都会作一些优化,虚拟使用-XX:+OptimizeStringConcat 打开字符串拼接优化,(默认状况下是打开的)。 若是采用如下代码,虽然看是跟上面的代码片断差很少,但虚拟机没法识别这种字符串拼接模式,性能会降低不少
StringBuilder sb = new StringBuilder(); sb.append(a); sb.append(b);
运行StringConcatTest类,代码以下
String a = "select u.id,u.name from user u"; String b=" where u.id=? " ; [@Benchmark](https://my.oschina.net/u/3268003) public String concat(){ String c = a+b; return c ; } [@Benchmark](https://my.oschina.net/u/3268003) public String concatbyOptimizeBuilder(){ String c = new StringBuilder().append(a).append(b).toString(); return c; } @Benchmark public String concatbyBuilder(){ //不会优化 StringBuilder sb = new StringBuilder(); sb.append(a); sb.append(b); return sb.toString(); }
有以下结果说明了虚拟机优化起了做用
Benchmark Mode Score Units c.i.c.c.StringConcatTest.concat avgt 25.747 ns/op c.i.c.c.StringConcatTest.concatbyBuilder avgt 90.548 ns/op c.i.c.c.StringConcatTest.concatbyOptimizeBuilder avgt 21.904 ns/op
能够看到concatbyBuilder是最慢的,由于没有被JVM优化
这里说的JVM优化,指的是虚拟机JIT优化,咱们会在第8章JIT优化说明
读者能够本身验证一下a+b+c这种字符串拼接性能,看一下是否被优化了
同StringBuilder相似的还有StringBuffer,主要功能都继承AbstractStringBuilder, 提供了线程安全方法,好比append方法,使用了synchronized关键字
@Override public synchronized StringBuffer append(String str) { //忽略其余代码 super.append(str); return this; }
几乎全部场景字符串拼接都不涉及到线程同步,所以StringBuffer已经不多使用了,如上的字符串拼接例子使用StringBuffer,
@Benchmark public String concatbyBuffer(){ StringBuffer sb = new StringBuffer(); sb.append(a); sb.append(b); return sb.toString(); }
输出以下
Benchmark Mode Score Units c.i.c.c.StringConcatTest.concatbyBuffer avgt 111.417 ns/op c.i.c.c.StringConcatTest.concatbyBuilder avgt 94.758 ns/op
能够看到,StringBuffer拼接性能跟StringBuilder相比性能并不差,这得益于虚拟机的"逃逸分析",也就是JIT在打开逃逸分析状况以及锁消除的状况下,有可能消除该对象上的使用synchronzied限定的锁。
逃逸分析 -XX:+DoEscapeAnalysis和 锁消除-XX:+EliminateLocks,详情参考本书第8章JIT优化
以下是一个锁消除的例子,对象obj只在方法内部使用,所以能够消除synchronized
void foo() { //建立一个对象 Object obj = new Object(); synchronized (obj) { doSomething(); } }
程序不该该依赖JIT的优化,尽管打开了逃逸分析和锁消除,但不能保证全部代码都会被优化,由于锁消除是在JIT的C2阶段优化的,做为程序员,应该在无关线程安全状况下,使用StringBuilder。
使用StringBuilder 拼接其余类型,尤为是数字类型,则性能会明显降低,这是由于数字类型转字符在JDK内部,须要作不少工做,一个简单的Int类型转为字符串,须要至少50行代码完成。咱们在第一章已经看到过了,这里再也不详细说明。当你用StringBuilder来拼接字符串,拼接数字的时候,你须要思考,是否须要一个这样的字符串。
咱们都知道浮点型变量在进行计算的时候会出现丢失精度的问题。以下一段代码
System.out.println(0.05 + 0.01); System.out.println(1.0 - 0.42);
输出: 0.060000000000000005 0.5800000000000001
能够看到在Java中进行浮点数运算的时候,会出现丢失精度的问题。那么咱们若是在进行商品价格计算的时候,就会出现问题。颇有可能形成咱们手中有0.06元,却没法购买一个0.05元和一个0.01元的商品。由于如上所示,他们两个的总和为0.060000000000000005。这无疑是一个很严重的问题,尤为是当电商网站的并发量上去的时候,出现的问题将是巨大的。可能会致使没法下单,或者对帐出现问题。
一般有俩个方法来解决这种问题,若是能用long来表示帐户余额以分为单位,这是效率最高的。若是不能,则只能使用BigDecimal类来解决这类问题。
BigDecimal a = new BigDecimal("0.05"); BigDecimal b = new BigDecimal("0.01"); BigDecimal ret = a.add(b); System.out.println(ret.toString());
经过字符串来构造BigDecimal,才能保证精度不丢失,若是使用new BigDecimal(0.05),则由于0.05自己精度丢失,使得构造出来的BigDecimal也丢失精度。
BigDecimal能保证精度,但计算会有必定性能影响,以下是测试余额计算,用long表示分,用BigDecimal表示元的性能对比
BigDecimal a = new BigDecimal("0.05"); BigDecimal b = new BigDecimal("0.01"); long c = 5; long d = 1; @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE) public long addByLong() { return (c + d); } @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE) public BigDecimal addByBigDecimal() { return a.add(b); }
在个人机器行,上面代码都能进行精确计算,经过JMH,测试结果以下
Benchmark Mode Score Units c.i.c.c.BigDecimalTest.addByBigDecimal avgt 8.373 ns/op c.i.c.c.BigDecimalTest.addByLong avgt 2.984 ns/op
因此在项目里,若是涉及精度结算,不要使用double,能够考虑用BigDecmal,也可使用long来完成精度计算,具备良好的性能,分布式或者微服务场景,考虑到序列化和反序列化,long也是能被全部序列化框架识别的