java虚拟机字符串的拼接

java虚拟机字符串的拼接

时间:20180228


1.不可变的String


在java中String对象时不可变的(Immutable)。在代码中,可以创建多个某一String对象的别名,但这些别名都是相同的引用。任何一个字符串的创建都会存放到常量池里。

比如s1和s2都是"oEffective"对象的别名,别名保存着到真实对象的引用。运行结果为True,即时s1 = s2。

  String s1 = "oEffective";
  String s2 = s1;
  system.out.println("s1 and s2 has the same reference = " + (s1 == s2));

String类中每一个看起来会修改String值的方法,实际上都是创建了一个全新的String对象,以包含修改后的字符串内容。

package com.test5;
public class StringTest {
    public static void main(String[] args) {
        String str1 = "oEffective";
        String str2 = "oEffective";
        String str3 = "pEffective";
        System.out.println("str1=str2 " + (str1 == str2));
    }
}

上述代码中将在常量池Constant pool中创建两个String对象,一个的值是oEffective和pEffectivce,而不是三个对象,原因是运行常量池中的结构可以类似的立即为:StringTable:HashSet,不能数据重复,两份oEffectivc在常量池中只存有一份,因此地址相同,引用也相同,str1、str2正式指向oEffective的引用(地址)。因此str1 == str2 为true;

为什么str1、str2正式指向oEffective的引用(地址)?
因为str1、str2为main方法的局部变量,是存在与虚拟机栈中的局部变量表中的。局部变量表:存放编译器各种基本类型引用、对象引用。

javac StringTest.java
javap -verbose StringTest.class


反编译结果

2.重载 “+”


在java中,唯一被重载的运算符就是用于String的“+”与“+=”,除此之外,java不允许程序员重载其他运算符。

字符串拼接剖析
既然String对象不可变,那么多个(三个及三个以上)字符串拼接必然产生多余的中间String对象。

        String str1 = "Effective";
        String str2 = "Study";
        String str3 = "Math";
        String info = str1 + str2 + str3;

执行上述过程得到info,就会将str1和str2拼接生成临时的一个String对象t1,而且t1创建后,内容为EffectiveStudy,然后他t1和str3拼接生成最终我们需要的info对象,这其中,产生了一个中间t1,而且t1创建之后没有主动回收,势必会占用一定的空间。如果有很多的字符串拼接(多见于对象的toString的调用),那么代价就更大了,性能一下会下降很多。
编译器的优化处理
虽然会存在上述的性能代价,虚拟机会进行优化,优化进行在编译器编译.java文件到字节流的过程中。
一个java程序如果想运行起来,需要经过两个时期,编译和运行。在编译阶段,java编译器会将java文件转换成字节码。在运行阶段,Java虚拟机运行编译生成的字节码文件(.class文件)。通过这样两个时期,Java做到了所谓的一处编译,处处运行。
我们试验一下编译器都做了哪些优化,我们制造一段可能会出现性能代价的代码。

package com.test5;

public class Concatenation {
    public static void main(String[] args) {
        String str1 = "Effective";
        String str2 = "Study";
        String str3 = "Math";
        String info = str1 + str2 + str3;
        System.out.println(info);
    }
}

使用Javac对Concatenation .java进行编译得到Concatenation .class(cmd中执行Javac Concatenation .java)。
然后反编译Concatenation .class(在cmd中执行Javap -verbose Concatenation .class)

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=5, args_size=1
         0: ldc           #2                  // String Effective
         2: astore_1
         3: ldc           #3                  // String Study
         5: astore_2
         6: ldc           #4                  // String Math
         8: astore_3
         9: new           #5                  // class java/lang/StringBuilder
        12: dup
        13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        16: aload_1
        17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        20: aload_2
        21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        24: aload_3
        25: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        28: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        31: astore        4
        33: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        36: aload         4
        38: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        41: return
      LineNumberTable:
        line 5: 0
        line 6: 3
        line 7: 6
        line 8: 9
        line 9: 33
        line 10: 41
}
SourceFile: "Concatenation.java"

其中,ldc,astore等为java字节码的指令,类似汇编指令。后面的注释使用了Java相关的内容进行了说明。我们可以看到上面很多StringBuilder,但是我们在java代码中并没有显示地调用,这就是Java编译器做的优化,当java编译器遇到字符串拼接时候,就会创建一个StringBuilder对象,后面的拼接,实际上是调用StringBuilder对象append方法。这样我们就不会担心上述的问题了。

仅靠编译器优化就能万事大吉 了?
既然编译器帮我们做了优化,是不是仅仅依靠编译器的优化就够了呢?当然不是。
下面来看一段为优化性能地的代码。

package com.test5;

public class StringPlus {
    public void stringPlusString(String[] values) {
        String result = "";
        for(int i = 0; i < values.length; i++) {
            result += values[i];
        }
        System.out.println(result);
    }
}

使用javac编译,并使用javap查看

  public void stringPlusString(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=4, args_size=2
         0: ldc           #2                  // String
         2: astore_2
         3: iconst_0
         4: istore_3
         5: iload_3
         6: aload_1
         7: arraylength
         8: if_icmpge     38
        11: new           #3                  // class java/lang/StringBuilder
        14: dup
        15: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        18: aload_2
        19: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        22: aload_1
        23: iload_3
        24: aaload
        25: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        28: invokevirtual #6                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        31: astore_2
        32: iinc          3, 1
        35: goto          5
        38: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
        41: aload_2
        42: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        45: return
      LineNumberTable:
        line 5: 0
        line 6: 3
        line 7: 11
        line 6: 32
        line 9: 38
        line 10: 45
      StackMapTable: number_of_entries = 2
        frame_type = 253 /* append */
          offset_delta = 5
          locals = [ class java/lang/String, int ]
        frame_type = 250 /* chop */
          offset_delta = 32
}
SourceFile: "StringPlus.java"

其中8: if_icmpge 38 和35: goto 5构成了一个循环。8: if_icmpge 38的意思是如果JVM操作数栈的整数对比大于等于(i < values.length的相反结果)成立,则跳到第38行(System.out)。35: goto 5则表示直接跳到第5行。
虽然经过java编译器优化之后会生成一个StringBuilder对象,但是这里StringBuilder对象创建却发生在虚幻之间,也就意味着有N次循环会创建N个StringBuilder对象,这样明显不好,赤裸裸的低水平代码。

稍微优化一下,瞬间提升逼格。

package com.test5;

public class StringPlus {
    public void stringPlusString(String[] values) {
        StringBuilder str = new StringBuilder();
        for(int i = 0;i < values.length; i++) {
            str.append(values[i]);
        }
    }
}

对应编译后的信息:

 public void stringPlusString(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=4, args_size=2
         0: new           #2                  // class java/lang/StringBuilder
         3: dup
         4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
         7: astore_2
         8: iconst_0
         9: istore_3
        10: iload_3
        11: aload_1
        12: arraylength
        13: if_icmpge     30
        16: aload_2
        17: aload_1
        18: iload_3
        19: aaload
        20: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        23: pop
        24: iinc          3, 1
        27: goto          10
        30: return
      LineNumberTable:
        line 5: 0
        line 6: 8
        line 7: 16
        line 6: 24
        line 9: 30
      StackMapTable: number_of_entries = 2
        frame_type = 253 /* append */
          offset_delta = 10
          locals = [ class java/lang/StringBuilder, int ]
        frame_type = 250 /* chop */
          offset_delta = 19
}
SourceFile: "StringPlus.java"

从上面可以看出,13: if_icmpge 30和27: goto 10构成了一个loop循环,而0: new #5位于循环之外,所以不会多次创建StringBuilder.

总的来说,在循环体中需要尽量避免隐式或者显示创建StringBuilder对象,所以了解代码如何编译,内部如何执行的人。写的代码档次都比较高。

参考https://droidyue.com/blog/2014/08/30/java-details-string-concatenation/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355