美文网首页
java虚拟机字符串的拼接

java虚拟机字符串的拼接

作者: pianpianboy | 来源:发表于2018-02-28 14:54 被阅读0次

    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/

    相关文章

      网友评论

          本文标题:java虚拟机字符串的拼接

          本文链接:https://www.haomeiwen.com/subject/rhebxftx.html