问题
:如下2个方法 fun1
和 fun2
,都是在做同样的事情,手动拼接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");
}
有编程经验的人都知道,肯定是使用了 StringBuilder
的 func2
效率更高。先看一下运行结果:
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的方式,为何效率差距这么多呢?
具体看一下 fun1
和 fun2
生成的字节码内容的区别(忽略了不重要的 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 效率更高。
网友评论