美文网首页
Java字符串拼接性能问题

Java字符串拼接性能问题

作者: 一个追寻者的故事 | 来源:发表于2020-05-20 11:22 被阅读0次

    问题:如下2个方法 fun1fun2,都是在做同样的事情,手动拼接15000个A,哪个效率高?

        private static void func1(){
            long start = System.currentTimeMillis();
            String str = "";
            for (int i = 0; i < 15000; i++){
                str += "A";
            }
            long end = System.currentTimeMillis();
            System.out.println("time of function one: " + (end - start) + "ms");
        }
    
        private static void func2(){
            long start = System.currentTimeMillis();
            StringBuilder builder = new StringBuilder();
            for (int i = 0; i < 15000; i++){
                builder.append('A');
            }
            String str = builder.toString();
            long end = System.currentTimeMillis();
            System.out.println("time of function two: " + (end - start) + "ms");
        }
    

    有编程经验的人都知道,肯定是使用了 StringBuilderfunc2 效率更高。先看一下运行结果:

    time of function one: 206ms
    time of function two: 1ms
    

    我们今天就来探讨一下,为什么高,探讨这问题之前,我们先来看一下,下面这段代码如何执行的:

        public static void fun3() {
            String a = "A";
            a = a + "B";
        }
    

    使用 javap 查看生成的字节码文件可知,在编译期间,自动转化成StringBuilder的实现方式。

      public static void fun3();
        descriptor: ()V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=1, args_size=0
             0: ldc           #16                 // String A
             2: astore_0
             3: new           #3                  // class java/lang/StringBuilder
             6: dup
             7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
            10: aload_0
            11: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
            14: ldc           #17                 // String B
            16: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
            19: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
            22: astore_0
            23: return
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                3      21     0     a   Ljava/lang/String;
    

    既然 + 已经默认转换成StringBuilder的方式,为何效率差距这么多呢?

    具体看一下 fun1fun2 生成的字节码内容的区别(忽略了不重要的 system.out.println等信息):

    public static void func1();
        descriptor: ()V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=2, args_size=0
             0: ldc           #13                 // String
             2: astore_0
             3: iconst_0
             4: istore_1
             5: iload_1
             6: sipush        15000
             9: if_icmpge     38
            12: new           #3                  // class java/lang/StringBuilder
            15: dup
            16: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
            19: aload_0
            20: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
            23: ldc           #16                 // String A
            25: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
            28: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
            31: astore_0
            32: iinc          1, 1
            35: goto          5
            38: return
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                5      33     1     i   I
                3      36     0   str   Ljava/lang/String;
    
    public static void func2();
        descriptor: ()V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=2, args_size=0
             0: new           #3                  // class java/lang/StringBuilder
             3: dup
             4: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
             7: astore_0
             8: iconst_0
             9: istore_1
            10: iload_1
            11: sipush        15000
            14: if_icmpge     30
            17: aload_0
            18: bipush        65
            20: invokevirtual #14                 // Method java/lang/StringBuilder.append:(C)Ljava/lang/StringBuilder;
            23: pop
            24: iinc          1, 1
            27: goto          10
            30: aload_0
            31: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
            34: astore_1
            35: return
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
               10      20     1     i   I
                8      28     0 builder   Ljava/lang/StringBuilder;
               35       1     1   str   Ljava/lang/String;
    

    通过字节码可知:fun1 每次的循环,都会创建一个StringBuilder对象,循环结束时创建String对象(toString方法),大量对象创建肯定消耗性能,而且每次循环结束的话,也会伴有对象不会再引用的状态(StringBuilder、String),当然如果到一定量的话,还可能会触发垃圾收集。这些都是耗时的点。 而fun2的循环里,只有append方法的调用,并无对象的创建(如果不考虑StringBuilder 内 数组扩容的话)。

    我们监视一下方法运行时的状态:-XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:./gc.log

    func1

    Java HotSpot(TM) 64-Bit Server VM (25.191-b12) for bsd-amd64 JRE (1.8.0_191-b12), built on Oct  6 2018 08:37:07 by "java_re" with gcc 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00)
    Memory: 4k page, physical 8388608k(437048k free)
    
    /proc/meminfo:
    
    CommandLine flags: -XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC 
    2020-05-20T11:10:57.339-0800: 0.145: [GC (Allocation Failure) [PSYoungGen: 33280K->448K(38400K)] 33280K->448K(125952K), 0.0031754 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
    2020-05-20T11:10:57.347-0800: 0.152: [GC (Allocation Failure) [PSYoungGen: 33728K->512K(71680K)] 33728K->520K(159232K), 0.0013694 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    2020-05-20T11:10:57.368-0800: 0.173: [GC (Allocation Failure) [PSYoungGen: 67072K->448K(71680K)] 67080K->464K(159232K), 0.0013071 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
    2020-05-20T11:10:57.375-0800: 0.180: [GC (Allocation Failure) [PSYoungGen: 67008K->496K(138240K)] 67024K->520K(225792K), 0.0014076 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    2020-05-20T11:10:57.413-0800: 0.219: [GC (Allocation Failure) [PSYoungGen: 133616K->517K(138240K)] 133640K->541K(225792K), 0.0017105 secs] [Times: user=0.00 sys=0.01, real=0.00 secs] 
    2020-05-20T11:10:57.427-0800: 0.232: [GC (Allocation Failure) [PSYoungGen: 133637K->517K(267264K)] 133661K->549K(354816K), 0.0012731 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    2020-05-20T11:10:57.511-0800: 0.317: [GC (Allocation Failure) [PSYoungGen: 266757K->32K(267264K)] 266789K->477K(354816K), 0.0017352 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    Heap
     PSYoungGen      total 267264K, used 165463K [0x0000000795580000, 0x00000007b5f80000, 0x00000007c0000000)
      eden space 266240K, 62% used [0x0000000795580000,0x000000079f70ded8,0x00000007a5980000)
      from space 1024K, 3% used [0x00000007a5980000,0x00000007a5988000,0x00000007a5a80000)
      to   space 1024K, 0% used [0x00000007b5e80000,0x00000007b5e80000,0x00000007b5f80000)
     ParOldGen       total 87552K, used 445K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000)
      object space 87552K, 0% used [0x0000000740000000,0x000000074006f498,0x0000000745580000)
     Metaspace       used 3084K, capacity 4500K, committed 4864K, reserved 1056768K
      class space    used 339K, capacity 388K, committed 512K, reserved 1048576K
    
    

    func2

    Java HotSpot(TM) 64-Bit Server VM (25.191-b12) for bsd-amd64 JRE (1.8.0_191-b12), built on Oct  6 2018 08:37:07 by "java_re" with gcc 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00)
    Memory: 4k page, physical 8388608k(251664k free)
    
    /proc/meminfo:
    
    CommandLine flags: -XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC 
    Heap
     PSYoungGen      total 38400K, used 3329K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000)
      eden space 33280K, 10% used [0x0000000795580000,0x00000007958c0678,0x0000000797600000)
      from space 5120K, 0% used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000)
      to   space 5120K, 0% used [0x0000000797600000,0x0000000797600000,0x0000000797b00000)
     ParOldGen       total 87552K, used 0K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000)
      object space 87552K, 0% used [0x0000000740000000,0x0000000740000000,0x0000000745580000)
     Metaspace       used 3045K, capacity 4500K, committed 4864K, reserved 1056768K
      class space    used 333K, capacity 388K, committed 512K, reserved 1048576K
    

    从虚拟机日志里验证了我们的想法,func1 不仅占用大量内存,而且期间还触发了 GC。到此,基本上说明白了为什么 fun2 效率更高。

    相关文章

      网友评论

          本文标题:Java字符串拼接性能问题

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