`
bsr1983
  • 浏览: 1100173 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

深入分析Java使用+和StringBuilder进行字符串拼接的差异

阅读更多

       今天看到有网友在我的博客留言,讨论javaString在进行拼接时使用+StringBuilderStringBuffer中的执行速度差异很大,而且之前看的书上说java在编译的时候会自动将+替换为StringBuilderStringBuffer,但对于这些我都没有做深入的研究,今天准备花一点时间,仔细研究一下。

       首先看一下java编译器在编译的时候自动替换+StringBuilderStringBuffer的部分,代码如下。

       测试环境为win764位系统,8G内存,CPU i5-3470JDK版本为32位的JDK1.6.0_38

       第一次使用的测试代码为:

         

  public static void main(String[] args) {
       // TODO Auto-generated method stub
       String demoString="";
       int execTimes=10000;
       if(args!=null&&args.length>0)
       {
           execTimes=Integer.parseInt(args[0]);
       }
       System.out.println("execTimes="+execTimes);
       long starMs=System.currentTimeMillis();
       for(int i=0;i<execTimes;i++)
       {
           demoString=demoString+i;
       }
       long endMs=System.currentTimeMillis();
       System.out.println("+ exec millis="+(endMs-starMs));
    }

 

   输入不同参数时的执行时间如下:

C:\>java StringAppendDemo 100
execTimes=100
+ exec millis=0
C:\>java StringAppendDemo 1000
execTimes=1000
+ exec millis=6
C:\>java StringAppendDemo 10000
execTimes=10000
+ exec millis=220
C:\>java StringAppendDemo 100000
execTimes=100000
+ exec millis=44267

 

可以看到,输入的参数为10000100000时,其执行时间从0.2秒到了44秒。

我们先使用javap命令看一下编译后的代码:

javap –c StringAppendDemo

这里我摘录了和循环拼接字符串有关的那部分代码,具体为:

  

51:  lstore_3
  52:  iconst_0
  53:  istore  5
  55:  iload   5
  57:  iload_2
  58:  if_icmpge       87
  61:  new     #5; //class java/lang/StringBuilder
  64:  dup
  65:  invokespecial   #6; //Method java/lang/StringBuilder."<init>":()V
  68:  aload_1
  69:  invokevirtual   #8; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  72:  iload   5
  74:  invokevirtual   #9; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  77:  invokevirtual   #10; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  80:  astore_1
  81:  iinc    5, 1
  84:  goto    55

 

可以看到,之前的+的确已经被编译为了StringBuilder对象的append方法。通过这里的字节码可以看到,对于每一个+都将被替换为一个StringBuilder而不是我所想象的只生成一个对象。也就是说,如果有10000+号就会生成10000StringBuilder对象。具体参看上面字节码的第84行,此处是执行完一次循环以后,再次跳转到55行去执行。

接着,我们把再写一个使用StringBuilder直接实现的方式,看看有什么不一样。

具体代码为:

public class StringBuilderAppendDemo {
       public static void main(String[] args) {
       // TODO Auto-generated method stub
       String demoString="";
       int execTimes=10000;
       if(args!=null&&args.length>0)
       {
           execTimes=Integer.parseInt(args[0]);
       }
       System.out.println("execTimes="+execTimes);
       long starMs=System.currentTimeMillis();
       StringBuilder strBuilder=new StringBuilder();
       for(int i=0;i<execTimes;i++)
       {
           strBuilder.append(i);
       }
       long endMs=System.currentTimeMillis();
       System.out.println("StringBuilder exec millis="+(endMs-starMs));
    }
}

 

和上次一样的参数,看看执行时间的差异

C:\>java StringBuilderAppendDemo 100
execTimes=100
StringBuilder exec millis=0
C:\>java StringBuilderAppendDemo 1000
execTimes=1000
StringBuilder exec millis=1
C:\>java StringBuilderAppendDemo 10000
execTimes=10000
StringBuilder exec millis=1
C:\>java StringBuilderAppendDemo 100000
execTimes=100000
StringBuilder exec millis=5

 

可以看到,这里的执行次数上升以后,执行时间并没有出现大幅度的增加,那我们在看一下编译后的字节码。

51:  lstore_3
 52:  new     #5; //class java/lang/StringBuilder
 55:  dup
 56:  invokespecial   #6; //Method java/lang/StringBuilder."<init>":()V
 59:  astore  5
 61:  iconst_0
 62:  istore  6
 64:  iload   6
 66:  iload_2
 67:  if_icmpge       84
 70:  aload   5
 72:  iload   6
 74:  invokevirtual   #9; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
 77:  pop
 78:  iinc    6, 1
 81:  goto    64

 

通过字节码可以看到,整个循环拼接过程中,只在56行对StringBuilde对象进行了一次初始化,以后的拼接操作的循环都是从64行开始,然后到81行进行goto 64再次循环。

为了证明我们的推断,我们需要看看虚拟机中是否是这么实现的。

参考代码:http://www.docjar.com/html/api/com/sun/tools/javac/jvm/Gen.java.html

具体的方法,标红的地方就是在语法树处理过程中的一个用来处理字符串拼接“+”号的例子,其他部分进行的处理也类似,我们只保留需要的部分

 

public void visitAssignop(JCAssignOp tree) {
 OperatorSymbol operator = (OperatorSymbol) tree.operator;
 Item l;
 if (operator.opcode == string_add) {
 // Generate code to make a string buffer
 makeStringBuffer(tree.pos());
 
 // Generate code for first string, possibly save one
 // copy under buffer
 l = genExpr(tree.lhs, tree.lhs.type);
 if (l.width() > 0) {
 code.emitop0(dup_x1 + 3 * (l.width() - 1));
 }
 
 // Load first string and append to buffer.
 l.load();
 appendString(tree.lhs);
 
 // Append all other strings to buffer.
 appendStrings(tree.rhs);
 
 // Convert buffer to string.
 bufferToString(tree.pos());
 }
 剩余代码已删除。

 

而具体把+转换为StringBuilder的方法为:


 

void makeStringBuffer(DiagnosticPosition pos) {
 code.emitop2(new_, makeRef(pos, stringBufferType));
 code.emitop0(dup);
 callMethod(
 pos, stringBufferType, names.init, List.<Type>nil(), false);
 }
 

 

看标红出的代码可以知道,此处调用了stringBufferTypeinit方法来进行初始化。

看到此处有同学一定会有疑问,刚刚的字节码不是显示替换成StringBuilder了吗?原因在这里:

protected Gen(Context context)95行)这个方法的代码,发现其中包含了stringBufferType变量的初始化:
stringBufferType = target.useStringBuilder() ? syms.stringBuilderType
                : syms.stringBufferType;108109110行)
通过一个三目运算符,根据当前的编译的目标JDK是否启用了StringBuilder来设置stringBufferType的真正类型。
回到处理“+”的代码,调用完makeStringBuffer方法后接着调用appendStrings方法和bufferToString方法。具体代码如下

 

/** Add all strings in tree to string buffer.
 */
 void appendStrings(JCTree tree) {
 tree = TreeInfo.skipParens(tree);
 if (tree.getTag() == JCTree.PLUS && tree.type.constValue() == null) {
 JCBinary op = (JCBinary) tree;
 if (op.operator.kind == MTH &&
 ((OperatorSymbol) op.operator).opcode == string_add) {
 appendStrings(op.lhs);
 appendStrings(op.rhs);
 return;
 }
 }
 genExpr(tree, tree.type).load();
 appendString(tree);
 }
 
 /** Convert string buffer on tos to string.
 */
 void bufferToString(DiagnosticPosition pos) {
 callMethod(
 pos,
 stringBufferType,
 names.toString,
 List.<Type>nil(),
 false);
 }
 

 

这里其实就是将字符串进行了缓存,接着通过调用stringBufferTypetoString()方法把StringBuilder中的字符转换为一个字符串对象。

接着我们通过visualvm工具看看上述两个例子运行过程中的内存使用和垃圾回收情况,visualvm工具路径为JDK根目录\bin\jvisualvm.exe

执行使用+操作符进行拼接的监视情况如下



 

可以看到在运行过程中,虚拟机进行了52871GC操作共耗费了49.278s,也就是说,运行时间的很大一部分是花在了垃圾回收上。

内存使用情况如下:



 

可以看到内存的占用大小也在忽上忽下,同样是垃圾回收的表现。

至于第二个例子,因为运行时间仅仅在4毫秒所有,vistalvm还来不及捕捉就执行完毕了,没有捕捉到相关的执行数据。

 

    综上所述,如果在编写代码的过程中大量使用+进行字符串评价还是会对性能造成比较大的影响,但是使用的个数在1000以下还是可以接受的,大于10000的话,执行时间将可能超过1s,会对性能产生较大影响。如果有大量需要进行字符串拼接的操作,最好还是使用StringBufferStringBuilder进行。

  • 大小: 41.3 KB
  • 大小: 33.6 KB
18
14
分享到:
评论
19 楼 qqyumidi123 2014-11-20  
咳..  这种纠结和博文标题不匹配吧,第一个代码里面 String demoString="";  改成StringBuffer试试。
18 楼 cry615 2013-10-11  
分析挺到位的,详细有据。
17 楼 bsr1983 2013-09-09  
youbl 写道
上面字节码的第88行

我看了半天字节码,一直找不到88在哪

此处应该是84行,之前有笔误,谢谢指出!
16 楼 youbl 2013-09-09  
上面字节码的第88行

我看了半天字节码,一直找不到88在哪
15 楼 runfriends 2013-09-05  
须等待 写道
sessionsong 写道
连+ 的时候 如:
String a="a";
String b="b";
String d="d";
String c = a+b+d;
他只会new 一个stringBuilder对象!并进行2次append操作,能后在调用stringbuilder的tostring方法,返回给string对象
如果是for 循环中的+, 每循环一次new 一个Stringbuilder对象!


那是编译级别的常量,一般在应用中都不会拿着100000个编译常量来+吧?


对于纯常量计算,编译器会完成,不会在运行期计算

所以编译完了c的值就是"abd"
再返编译回来就是定义了四个字符串,都是字面值,没有+,也没有StringBuilder
14 楼 sessionsong 2013-09-05  
须等待 写道
sessionsong 写道
连+ 的时候 如:
String a="a";
String b="b";
String d="d";
String c = a+b+d;
他只会new 一个stringBuilder对象!并进行2次append操作,能后在调用stringbuilder的tostring方法,返回给string对象
如果是for 循环中的+, 每循环一次new 一个Stringbuilder对象!


那是编译级别的常量,一般在应用中都不会拿着100000个编译常量来+吧?


嗯! 那到时,这只是在讨论问题,实际中确实是不会这样
13 楼 须等待 2013-09-05  
sessionsong 写道
连+ 的时候 如:
String a="a";
String b="b";
String d="d";
String c = a+b+d;
他只会new 一个stringBuilder对象!并进行2次append操作,能后在调用stringbuilder的tostring方法,返回给string对象
如果是for 循环中的+, 每循环一次new 一个Stringbuilder对象!


那是编译级别的常量,一般在应用中都不会拿着100000个编译常量来+吧?
12 楼 sessionsong 2013-09-04  
连+ 的时候 如:
String a="a";
String b="b";
String d="d";
String c = a+b+d;
他只会new 一个stringBuilder对象!并进行2次append操作,能后在调用stringbuilder的tostring方法,返回给string对象
如果是for 循环中的+, 每循环一次new 一个Stringbuilder对象!
11 楼 yunhua_lee 2013-09-04  
你的字符串太短了,测试起来效果差异不明显,如果你每次都使用1K的字符串去加,我估计不用到10000次,1000次差异就可能很大
10 楼 runfriends 2013-09-04  
bsr1983 写道
runfriends 写道
连+,最后一个分号只创建一个StringBuilder
一旦连接操作由多条语句执行,就是一条语句一个StringBuilder

这个没有验证,有空验证一下


把连+的字节码反编译一下,就是只创建一个StringBuilder
9 楼 runfriends 2013-09-04  
bsr1983 写道
runfriends 写道
连+,最后一个分号只创建一个StringBuilder
一旦连接操作由多条语句执行,就是一条语句一个StringBuilder

这个没有验证,有空验证一下


可以在连+那一行加个断点,逐行debug,就知道了
8 楼 sessionsong 2013-09-04  
  谢谢你的解答! 写的很详细
将+替换为StringBuffer或StringBuilder的append操作运行时耗时耗时相差大的原因是
+ 的时候需要频繁的new StringBuilder(StringBuffer)对象和初始化,能后在调用append()方法,而直接使用append方法  不需要频繁的new 和初始化!
在使用+的时候 时间大多数都消耗在new StringBuilder(StringBuffer)对象和初始化了
7 楼 qincidong 2013-09-04  
分析的不错,确实在拼接较少的情况下使用+方便,拼接的很多的情况一般也就在循环中,使用StringBuffer或StringBuilder来处理。
6 楼 liu78778 2013-09-04  
引用
综上所述,如果在编写代码的过程中大量使用+进行字符串评价还是会对性能造成比较大的影响,但是使用的个数在1000以下还是可以接受的,大于10000的话,执行时间将可能超过1s,会对性能产生较大影响。


结论股沟精确,只有在循环时使用+来拼接会产生此类问题,如果在非循环下,+和StringBuilder之类的基本没有任何分别。
5 楼 bsr1983 2013-09-03  
zidafone 写道
拼10个20个的话,还是+的简洁比较有价值,对吗?

个人认为应该不至于影响性能
4 楼 bsr1983 2013-09-03  
runfriends 写道
连+,最后一个分号只创建一个StringBuilder
一旦连接操作由多条语句执行,就是一条语句一个StringBuilder

这个没有验证,有空验证一下
3 楼 runfriends 2013-09-03  
连+,最后一个分号只创建一个StringBuilder
一旦连接操作由多条语句执行,就是一条语句一个StringBuilder
2 楼 zidafone 2013-09-03  
拼10个20个的话,还是+的简洁比较有价值,对吗?
1 楼 xucaishen 2013-09-03  
分析得不错,学习了。。。

相关推荐

Global site tag (gtag.js) - Google Analytics