"StringBuffer"和"String"性能大比拼

看了cherami写的使用 《String还是StringBuffer?》以及后面多位网友的评论,感觉这个问题有必要好好的深究一下,因此编写了一个简单的测试类和一个脚本来运行它。通过修改那个测试类为不同的参数,并且在不同的JDK上测试发现这个问题实在是一个非常有趣的问题。下面让我们开始吧。

第一步,准备工作

为了方便后面测试工作的进行,有必要编写一个简单的脚本:

     
     echo test by jdk1.2.2
/opt/java/jdk1.2.2/bin/javac StringTest.java
/opt/java/jdk1.2.2/bin/java StringTest
echo test by jdk1.3.1_09
/opt/java/jdk1.3.1_09/bin/javac StringTest.java
/opt/java/jdk1.3.1_09/bin/java StringTest
echo test by jdk1.4.2
/opt/java/jdk1.4.2/bin/javac StringTest.java
/opt/java/jdk1.4.2/bin/java StringTest


上面的脚本根据需要可以应用在windows或者linux上,我是在linux进行测试的,因此我把它保存为一个文件stringtest.sh,如果你在windows上测试,你可以保存为stringtest.bat。

注意:本文后面的运行结果都是连续运行多次并取其中一个比较平均和典型的样本值)

第二步,开始写代码

最开始我几乎没有怎么考虑就写出了如下的代码:

     
     public class StringTest {
  public static void main(String[] args) {
    long start=System.currentTimeMillis();
    for (int i=0; i<10000; i++) {
      String s="This is a "+"long test string for "+"different JDK  
      performance "+"testing.";    }
    long end=System.currentTimeMillis();
    System.out.println("Directly string contact:"+(end-start));      
    start=System.currentTimeMillis();
    for (int i=0; i<10000; i++) {
      StringBuffer buffer = new StringBuffer(); 
     buffer.append("This is a "); 
     buffer.append("long test string for ");
      buffer.append("different JDK performance "); 
     buffer.append("testing.");
      String ss=buffer.toString(); 
   }
    end=System.currentTimeMillis();
    System.out.println("StringBuffer contact:"+(end-start));
  }
}


运行结果:

     
     test by jdk1.2.2
Directly string contact:0
StringBuffer contact:120
test by jdk1.3.1_09
Directly string contact:1
StringBuffer contact:47
test by jdk1.4.2
Directly string contact:0
StringBuffer contact:53


呵呵,是不是大出意外?!!!我开始也是,但是别急,实际上我犯了一个错误,由于进行字符串+操作的都是字符串直接量,因此编译器在编译的时候进行了优化,所以String s="This is a "+"long test string for "+"different JDK performance "+"testing.";编译以后实际上变成了:String s="This is a long test string for different JDK performance testing.";,呵呵,这就是一个简单的赋值操作了,难怪所用的时间几乎没有了。

第三步,修改代码

     
     public class StringTest {
  public static void main(String[] args) {
    String s1="This is a ";
    String s2="long test string for ";
    String s3="different JDK performance ";
    String s4="testing."; 
           long start=System.currentTimeMillis();
    for (int i=0; i<10000; i++) {
      String s=s1+s2+s3+s4;
    }
    long end=System.currentTimeMillis();
    System.out.println("Directly string contact:"+(end-start));    
    start=System.currentTimeMillis();
    for (int i=0; i<10000; i++) {
      StringBuffer buffer = new StringBuffer();
      buffer.append(s1);
      buffer.append(s2);
      buffer.append(s3);
      buffer.append(s4);
      String ss=buffer.toString();
    }
    end=System.currentTimeMillis();
    System.out.println("StringBuffer contact:"+(end-start));
  }
}


运行结果:

     
     test by jdk1.2.2
Directly string contact:140
StringBuffer contact:123
test by jdk1.3.1_09
Directly string contact:32
StringBuffer contact:21
test by jdk1.4.2
Directly string contact:48
StringBuffer contact:37


从上面的结果看我们确实可以得到《使用String还是StringBuffer?》中的结论,而且还可以看出不同的JDK的版本的性能的差异还是比较大的,JDK1.2.2的性能最差,而JDK1.3.1的性能最好。

讨论结束了吗?呵呵,不要急,还远远没有结束呢。:)

第四步,减少循环次数(从10000次变成1000)

     
     public class StringTest {
  public static void main(String[] args) {
    String s1="This is a ";
    String s2="long test string for ";
    String s3="different JDK performance ";
    String s4="testing.";
            long start=System.currentTimeMillis();
    for (int i=0; i<1000; i++) {
      String s=s1+s2+s3+s4;
    }    
long end=System.currentTimeMillis();
    System.out.println("Directly string contact:"+(end-start));    
    start=System.currentTimeMillis();
    for (int i=0; i<1000; i++) {
      StringBuffer buffer = new StringBuffer();
      buffer.append(s1);
      buffer.append(s2);
      buffer.append(s3);
      buffer.append(s4);
      String ss=buffer.toString();
    }
    end=System.currentTimeMillis();
    System.out.println("StringBuffer contact:"+(end-start));
  }
}


运行结果:

     
     test by jdk1.2.2
Directly string contact:12
StringBuffer contact:19
test by jdk1.3.1_09
Directly string contact:9
StringBuffer contact:13
test by jdk1.4.2
Directly string contact:12
StringBuffer contact:18


你看到什么了?上面的结论又被推翻了,字符串直接连接操作的性能比使用StringBuffer的性能好,无论是在那个版本的JDK上,而且在JDK1.3.1上的性能依然是最高的。我们不禁想问为什么。很遗憾,我也不知道为什么。我的想法是上面两种程序的差异主要在于循环的次数,而次数的不同导致创建的对象的个数也就是内存的使用情况不同,另外一个不同就是运行次数不同而导致JIT进行运行时优化的强度不同。这两个因素只是我认为对程序结果肯定会有影响的因素,但是是如何影响的,我就不知道了。

到了这里,也许我们的故事该结束了,但是从上面的结果看,我感觉有必要再做一些测试。

第五步,将所有的中间结果串起来

     
     public class StringTest {
  public static void main(String[] args) {
    String s1="This is a ";
    String s2="long test string for ";
    String s3="different JDK performance ";
    String s4="testing."; 
           long start=System.currentTimeMillis();
    String s="";
    for (int i=0; i<1000; i++) {
      s+=s1+s2+s3+s4;
    }
    long end=System.currentTimeMillis();
    System.out.println("Directly string contact:"+(end-start)); 
   start=System.currentTimeMillis();
    StringBuffer buffer = new StringBuffer();
    for (int i=0; i<1000; i++) { 
     buffer.append(s1);
      buffer.append(s2);
      buffer.append(s3);
      buffer.append(s4);
      String ss=buffer.toString();
    }
    end=System.currentTimeMillis();
    System.out.println("StringBuffer contact:"+(end-start));
  }
}


运行结果:

     
     test by jdk1.2.2
Directly string contact:997
StringBuffer contact:13
test by jdk1.3.1_09
Directly string contact:1900
StringBuffer contact:21
test by jdk1.4.2
Directly string contact:2157
StringBuffer contact:11


我们终于看到使用StringBuffer的巨大优势了!而且你也许还会注意到一个非常有趣的现象:JDK1.3.1的性能似乎在这里表现得没有那么突出了,在StringBuffer甚至表现得最差了。非常有趣,不是吗?

第六步,再次减少循环次数(从1000次减少为500次)

     
     public class StringTest {
  public static void main(String[] args) {
    String s1="This is a ";
    String s2="long test string for ";
    String s3="different JDK performance ";
    String s4="testing.";
            long start=System.currentTimeMillis();
    String s="";
    for (int i=0; i<500; i++) {
      s+=s1+s2+s3+s4;
    }
    long end=System.currentTimeMillis();
    System.out.println("Directly string contact:"+(end-start));
    start=System.currentTimeMillis();
    StringBuffer buffer = new StringBuffer();
    for (int i=0; i<500; i++) {
      buffer.append(s1);
      buffer.append(s2);
      buffer.append(s3);
      buffer.append(s4);
      String ss=buffer.toString();
    }
    end=System.currentTimeMillis();
    System.out.println("StringBuffer contact:"+(end-start));
  }
}


运行结果:

     
     test by jdk1.2.2
Directly string contact:270
StringBuffer contact:9
test by jdk1.3.1_09
Directly string contact:251
StringBuffer contact:2
test by jdk1.4.2
Directly string contact:264
StringBuffer contact:2


呵呵,JDK1.3.1的性能优势又回来了!而且和1000次的情况相比,字符串直接操作的性能差距减小了。

而且上面给出的仅仅是一个样本值,如果你多运行几次会发现如下的现象:JDK1.2.2和JDK1.3.3的运行时间在每次运行的时候都差不多,但是JDK1.4.2的StringBuffer操作的运行时间的波动就比较大了,在我的环境下的波动范围竟然在1到16之间!

故事到这里,我不想再继续做更多的测试了,我的头脑里面已经没有谁好谁坏的结论了,我们有必要总结一下我们上面实验的结果了。

总结

我感觉从上面的一些实验来看,我们可以得到如下的一些结论:

1.String的+操作和StringBuffer不存在绝对的性能问题,但是可以得到的结论是,无论在那个版本的JDK上,如果是连接的内容非常的大和多,并且代码被执行的次数很多,那么不要犹豫,使用StringBuffer,这个性能差异是很大的。如果是连接比较短小的内容,那么使用你喜欢的方式吧,它们的性能没有明显的差异。

2.不同版本的JDK对这两种操作所做的优化是不同的,总体而言JDK1.3.1对它们的优化是比较好的。

我想提醒你的是不要得出如下的结论:

JDK1.3.1的性能是最好的。我们的结论只是针对我们测试的问题,对于其他的问题,JDK1.3.1的表现如何就不得而知了,但是我相信JDK1.3.1的总体性能比JDK1.2.2要好一些。

我想提醒大家最后注意的事情是:

1.不要盲目相信性能优化,用一句我以前在JR的某篇文章里面看到的话说就是:只有在你知道它确实是性能瓶颈的地方才去优化。

2.JDK的不同版本对于某些在某些方面进行了一些运行和编译优化,但是这种优化是不固定的,我们不要对那些优化寄予太大的期望,最根本的还是你的代码的性能。

网友评论

评论人:firebox

我觉得楼主不能以这样的测试方式来表明性能差异。java应用不是单纯的从你所说的这方面来讲的。一个虚拟机在长期的运作过程中,涉及到的是对内存的管理,+操作明显是增加了对象,内存垃圾会越来越多,而且当并发请求时,我想情况可能会更糟!

客人: jsyx

毫无疑义的测试。

用StringBuffer的意义不仅仅在连接速度上。

从服务器的角度来看,用StringBuffer的意义在于可以产生比直接连接少的多的垃圾对象。要知道gc运行次数/时间的降低,对于一个快速长期运行的程序来说是意义重大的

评论人:wolfsquare

重新看了一下,发现一个问题,就是根据我个人的经验使用jvm的currentTimeMillis()方法得到的数值是不精确的,至少在100毫秒的级别上是不准确的.建议作者有类似测试的话最好多做几次,取他们的平均值.

评论人:ljdrer

呵呵,可能是没有在开头明确说一下这个测试的目的,其实最初的目的是因为在项目做的时候上面要求不能使用字符串+的操作,因为性能不好,而且自己以前也确实看到过一些文章和书籍说过这确实是一个问题,但是后来想到JDK是不断在进行优化的,因此这个问题可能已经不存在了。

在这里补充说明一下测试的目的:

要测试的仅仅是:

     
     String s=s1+s2+s3+s4;
和 StringBuffer buffer=new StringBuffer();
buffer.append(s1);
buffer.append(s2);
buffer.append(s3);
String s=buffer.toString();


之间的性能差异,程序之所以进行循环是为了将这个差异放大,否则一条语句的执行时间的差异能有多大呢??

评论人:ljdrer

to wolfsquare:原文开头已经说得很清楚了:

"本文后面的运行结果都是连续运行多次并取其中一个比较平均和典型的样本值"

因此应该不会存在你所说的问题吧。

评论人:mosa

The methodology you mentioned is not fair enough. Given that you only run the loop with less than some hundred milliseconds,there're many factors that may bias the results. To name a few:

1. VM initialization, class loading and JIT time, which can easily amount to some 10 ms. If you test StringBuffer first and then String "+", the result may be different. You may first run both cases with a few loops and then begin real time collecting.

2. GC may take a few ms to some 10 ms, depends on different systems. For a collecting timeframe like some 10 ms, one more GC may contaminate the expected results. You may run Runtime.gc() before the StringBuffer test begins. But the behavior of Runtime.gc() may depend on different VMs.

For a modified step 3 and 4, you can conclude String "+" and StringBuffer append are with comparable performance. That's not surprising, because after you use "javap -c" to deassemble them into bytecode, you can find the two bytecode sequences are very similar (String "+" may even be more optimal in the bytecode level, because it utilizes StringBuffer.append's return value for chained concatenation). StringBuffer has the advantage that you can specify the initial capacity with your knowledge in the context (the default value is 16?), then you can avoid many capacity expansion which further relaxes GC and arraycopy overhead.

Step 5 is totally different from 3&4. The String "+" part creates much more objects (at least one String and one StringBuffer per loop and many subsequent objects like char arrays) than StringBuffer part. "javap -c" and "java -verbosegc" may help you solve the puzzle.

客人: finalarrow

你试着在初始实例化 StringBuffer 时设置 StringBuffer 的长度(当然是比测试的结果字符串大一点),你试一下会有什么结果?我没试过,不过,有一篇文章是讨论这个的,设置了长度后,StringBuffer就不用每次再去比较大小来分配地址,速度会快很多的。

这和Vector 的原理是一样的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值