我的问题是:在这个循环中创建了多少对象?有中间对象吗?我如何验证?
剧透:JVM不会试图省略循环中的中间对象 - 因此在使用纯连接时它们将被创建。
首先让我们看一下字节码。我使用@Eugene提供的性能测试,将它们编译为Java8和Java9。这是我们要比较的两种方法:
public String concatBuilder() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < howmany; ++i) {
sb.append(i);
}
return sb.toString();
}
public String concatPlain() {
String result = "";
for (int i = 0; i < howmany; ++i) {
result = result + i;
}
return result;
}
我的Java版本如下:
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
java version "9.0.4"
Java(TM) SE Runtime Environment (build 9.0.4+11)
Java HotSpot(TM) 64-Bit Server VM (build 9.0.4+11, mixed mode)
JMH版本为1.20
这是我从javap -c LoopTest.class
获得的输出:
concatBuilder()
方法明确利用StringBuilder
,在Java8和Java9中看起来完全相同:
public java.lang.String concatBuilder();
Code:
0: new
3: dup
4: invokespecial
7: astore_1
8: iconst_0
9: istore_2
10: iload_2
11: aload_0
12: getfield
15: if_icmpge 30
18: aload_1
19: iload_2
20: invokevirtual
23: pop
24: iinc 2, 1
27: goto 10
30: aload_1
31: invokevirtual
34: areturn
请注意,在循环内调用了
StringBuilder.append
,而在循环外调用了
StringBuilder.toString
。这很重要-这意味着不会创建任何中间对象。在java8字节码中有些不同:
Java8中的
concatPlain()
方法:
public java.lang.String concatPlain();
Code:
0: ldc
2: astore_1
3: iconst_0
4: istore_2
5: iload_2
6: aload_0
7: getfield
10: if_icmpge 38
13: new
16: dup
17: invokespecial
20: aload_1
21: invokevirtual
24: iload_2
25: invokevirtual
28: invokevirtual
31: astore_1
32: iinc 2, 1
35: goto 5
38: aload_1
39: areturn
你可以看到,在Java8中,
StringBuilder.append
和
StringBuilder.toString
都在循环语句内部被调用,这意味着
它甚至不尝试省略中间对象的创建!代码如下所示:
public String concatPlain() {
String result = "";
for (int i = 0; i < howmany; ++i) {
result = result + i;
result = new StringBuilder().append(result).append(i).toString();
}
return result;
}
这里解释了
concatPlain()
和
concatBuilder()
之间的性能差异(后者比前者快几千倍!)。在Java9中也存在同样的问题 - 它不会尝试避免循环内部的中间对象,但是它在循环内部做得比Java8略好(添加了性能结果)。
Java9中的
concatPlain()
方法:
public java.lang.String concatPlain();
Code:
0: ldc
2: astore_1
3: iconst_0
4: istore_2
5: iload_2
6: aload_0
7: getfield
10: if_icmpge 27
13: aload_1
14: iload_2
15: invokedynamic
20: astore_1
21: iinc 2, 1
24: goto 5
27: aload_1
28: areturn
这里是性能结果:
JAVA 8:
# Run complete. Total time: 00:02:18
Benchmark (howmany) Mode Cnt Score Error Units
LoopTest.concatBuilder 100000 avgt 5 2.098 ± 0.027 ms/op
LoopTest.concatPlain 100000 avgt 5 6908.737 ± 1227.681 ms/op
JAVA 9:
对于Java 9,有不同的策略定义为-Djava.lang.invoke.stringConcat
。我尝试了所有这些策略:
默认值(MH_INLINE_SIZED_EXACT):
# Run complete. Total time: 00:02:30
Benchmark (howmany) Mode Cnt Score Error Units
LoopTest.concatBuilder 100000 avgt 5 1.625 ± 0.015 ms/op
LoopTest.concatPlain 100000 avgt 5 4812.022 ± 73.453 ms/op
-Djava.lang.invoke.stringConcat=BC_SB
# Run complete. Total time: 00:02:28
Benchmark (howmany) Mode Cnt Score Error Units
LoopTest.concatBuilder 100000 avgt 5 1.501 ± 0.024 ms/op
LoopTest.concatPlain 100000 avgt 5 4803.543 ± 53.825 ms/op
-Djava.lang.invoke.stringConcat=BC_SB_SIZED
# Run complete. Total time: 00:02:17
Benchmark (howmany) Mode Cnt Score Error Units
LoopTest.concatBuilder 100000 avgt 5 1.546 ± 0.027 ms/op
LoopTest.concatPlain 100000 avgt 5 4941.226 ± 422.704 ms/op
-Djava.lang.invoke.stringConcat=BC_SB_SIZED_EXACT
# Run complete. Total time: 00:02:45
Benchmark (howmany) Mode Cnt Score Error Units
LoopTest.concatBuilder 100000 avgt 5 1.560 ± 0.073 ms/op
LoopTest.concatPlain 100000 avgt 5 11390.665 ± 232.269 ms/op
-Djava.lang.invoke.stringConcat=BC_SB_SIZED_EXACT
# Run complete. Total time: 00:02:16
Benchmark (howmany) Mode Cnt Score Error Units
LoopTest.concatBuilder 100000 avgt 5 1.616 ± 0.030 ms/op
LoopTest.concatPlain 100000 avgt 5 8524.200 ± 219.499 ms/op
-Djava.lang.invoke.stringConcat=MH_SB_SIZED_EXACT
# Run complete. Total time: 00:02:17
Benchmark (howmany) Mode Cnt Score Error Units
LoopTest.concatBuilder 100000 avgt 5 1.633 ± 0.058 ms/op
LoopTest.concatPlain 100000 avgt 5 8499.228 ± 972.832 ms/op
-Djava.lang.invoke.stringConcat=MH_INLINE_SIZED_EXACT(是默认值,但我决定明确设置以便于实验的清晰度)
# Run complete. Total time: 00:02:23
Benchmark (howmany) Mode Cnt Score Error Units
LoopTest.concatBuilder 100000 avgt 5 1.654 ± 0.015 ms/op
LoopTest.concatPlain 100000 avgt 5 4812.231 ± 54.061 ms/op
我决定调查内存使用情况,但除了Java9消耗更多内存外,没有发现任何有趣的东西。如果有人感兴趣,可以参考附带的截图。当然,这些截图是在实际性能测量之后制作的,而不是在测试期间制作的。
Java8 concatBuilder():
![Java8 concatBuilder()](https://istack.dev59.com/kZy61.webp)
Java8 concatPlain():
![enter image description here](https://istack.dev59.com/cM7qS.webp)
Java9 concatBuilder():
![enter image description here](https://istack.dev59.com/zGb1g.webp)
Java9 concatPlain():
所以,回答你的问题,我可以说无论是Java8还是Java9都无法避免在循环中创建中间对象。
更新:
正如@Eugene所指出的那样,裸字节码可能没有意义,因为JIT在运行时进行了许多优化,这对我来说看起来是合理的,因此我决定添加由JIT优化的代码输出(通过-XX:CompileCommand=print,*LoopTest.concatPlain
捕获)。
JAVA 8:
0x00007f8c2d216d29: callq 0x7f8c2d0fdea0
0x00007f8c2d216d2e: jmpq 0x7f8c2d216786
0x00007f8c2d216d33: mov %rdx,%rdx
0x00007f8c2d216d36: callq 0x7f8c2d0fa1a0
0x00007f8c2d216d3b: jmpq 0x7f8c2d2167e6
0x00007f8c2d216d40: mov %rbx,0x8(%rsp)
0x00007f8c2d216d45: movq $0xffffffffffffffff,(%rsp)
0x00007f8c2d216d4d: callq 0x7f8c2d0fdea0
0x00007f8c2d216d52: jmpq 0x7f8c2d21682d
0x00007f8c2d216d57: mov %rbx,0x8(%rsp)
0x00007f8c2d216d5c: movq $0xffffffffffffffff,(%rsp)
0x00007f8c2d216d64: callq 0x7f8c2d0fdea0
0x00007f8c2d216d69: jmpq 0x7f8c2d216874
0x00007f8c2d216d6e: mov %rbx,0x8(%rsp)
0x00007f8c2d216d73: movq $0xffffffffffffffff,(%rsp)
0x00007f8c2d216d7b: callq 0x7f8c2d0fdea0
0x00007f8c2d216d80: jmpq 0x7f8c2d2168bb
0x00007f8c2d216d85: callq 0x7f8c2d0faa60
0x00007f8c2d216d8a: jmpq 0x7f8c2d21693a
0x00007f8c2d216d8f: mov %rdx,0x8(%rsp)
0x00007f8c2d216d94: movq $0xffffffffffffffff,(%rsp)
0x00007f8c2d216d9c: callq 0x7f8c2d0fdea0
0x00007f8c2d216da1: jmpq 0x7f8c2d216a1c
0x00007f8c2d216da6: mov %rdx,0x8(%rsp)
0x00007f8c2d216dab: movq $0xffffffffffffffff,(%rsp)
0x00007f8c2d216db3: callq 0x7f8c2d0fdea0
0x00007f8c2d216db8: jmpq 0x7f8c2d216b08
0x00007f8c2d216dbd: mov %rdx,0x8(%rsp)
0x00007f8c2d216dc2: movq $0xffffffffffffffff,(%rsp)
0x00007f8c2d216dca: callq 0x7f8c2d0fdea0
0x00007f8c2d216dcf: jmpq 0x7f8c2d216bf8
0x00007f8c2d216dd4: mov %rdx,%rdx
0x00007f8c2d216dd7: callq 0x7f8c2d0fa1a0
0x00007f8c2d216ddc: jmpq 0x7f8c2d216c39
0x00007f8c2d216de1: mov %rax,0x8(%rsp)
0x00007f8c2d216de6: movq $0x23,(%rsp)
0x00007f8c2d216dee: callq 0x7f8c2d0fdea0
0x00007f8c2d216df3: jmpq 0x7f8c2d216cae
正如您所看到的,
StringBuilder::toString
在跳转之前被调用,这意味着所有操作都发生在循环内部。Java9 中的情况类似 - 在执行 goto 命令之前调用了
StringConcatHelper::newString
。
0x00007fa1256548a4: mov %ebx,%r13d
0x00007fa1256548a7: sub 0xc(%rsp),%r13d
0x00007fa1256548ac: test %r13d,%r13d
0x00007fa1256548af: jl 0x7fa125654b11
0x00007fa1256548b5: mov %r13d,%r10d
0x00007fa1256548b8: add %r9d,%r10d
0x00007fa1256548bb: mov 0x20(%rsp),%r11d
0x00007fa1256548c0: cmp %r10d,%r11d
0x00007fa1256548c3: jb 0x7fa125654b11
0x00007fa1256548c9: test %r9d,%r9d
0x00007fa1256548cc: jbe 0x7fa1256548ef
0x00007fa1256548ce: movsxd %r9d,%rdx
0x00007fa1256548d1: lea (%r12,%r8,8),%r10
0x00007fa1256548d5: lea 0x10(%r12,%r8,8),%rdi
0x00007fa1256548da: mov %rcx,%r10
0x00007fa1256548dd: lea 0x10(%rcx,%r13),%rsi
0x00007fa1256548e2: movabs $0x7fa11db9d640,%r10
0x00007fa1256548ec: callq %r10
0x00007fa1256548ef: cmp 0xc(%rsp),%ebx
0x00007fa1256548f3: jne 0x7fa125654cb9
0x00007fa1256548f9: mov 0x60(%r15),%rax
0x00007fa1256548fd: mov %rax,%r10
0x00007fa125654900: add $0x18,%r10
0x00007fa125654904: cmp 0x70(%r15),%r10
0x00007fa125654908: jnb 0x7fa125654aa5
0x00007fa12565490e: mov %r10,0x60(%r15)
0x00007fa125654912: prefetchnta 0x100(%r10)
0x00007fa12565491a: mov 0x18(%rsp),%rsi
0x00007fa12565491f: mov 0xb0(%rsi),%r10
0x00007fa125654926: mov %r10,(%rax)
0x00007fa125654929: movl $0xf80002da,0x8(%rax)
0x00007fa125654930: mov %r12d,0xc(%rax)
0x00007fa125654934: mov %r12,0x10(%rax)
0x00007fa125654938: mov 0x30(%rsp),%r10
0x00007fa12565493d: shr $0x3,%r10
0x00007fa125654941: mov %r10d,0xc(%rax)
0x00007fa125654945: mov 0x8(%rsp),%ebx
0x00007fa125654949: incl %ebx
0x00007fa12565494b: test %eax,0x1a8996af(%rip)
Java8
还是Java9
- 如果需要在循环中连接字符串,请使用StringBuilder
。 - Oleksandr Pyrohov