如果我说错了,请纠正我。在Java8中,出于性能原因,当通过"+“操作符连接多个字符串时,会调用StringBuffer。创建一堆中间字符串对象并污染字符串池的问题也得到了“解决”。
Java 9怎么样?有一个新特性被添加为Invokedynamic。还有一个更好地解决这个问题的新类,StringConcatFactory。
String result = "";
List<String> list = Arrays.asList("a", "b", "c");
for (String n : list) {
result+=n;
}
我的问题是:在这个循环中创建了多少对象?是否有中介对象?我该如何验证这一点呢?
发布于 2018-03-24 23:57:13
根据记录,这是一个JMH
测试...
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@State(Scope.Thread)
public class LoopTest {
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder().include(LoopTest.class.getSimpleName())
.jvmArgs("-ea", "-Xms10000m", "-Xmx10000m")
.shouldFailOnError(true)
.build();
new Runner(opt).run();
}
@Param(value = {"1000", "10000", "100000"})
int howmany;
@Fork(1)
@Benchmark
public String concatBuilder(){
StringBuilder sb = new StringBuilder();
for(int i=0;i<howmany;++i){
sb.append(i);
}
return sb.toString();
}
@Fork(1)
@Benchmark
public String concatPlain(){
String result = "";
for(int i=0;i<howmany;++i){
result +=i;
}
return result;
}
}
产生我没有预料到的结果(仅适用于这里显示的100000
):
LoopTest.concatPlain 100000 avgt 5 3902.711 ± 67.215 ms/op
LoopTest.concatBuilder 100000 avgt 5 1.850 ± 0.574 ms/op
发布于 2018-03-25 23:58:35
我的问题是:在这个循环中创建了多少对象?有没有中间对象?我如何验证这一点?
剧透:
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
获得的输出
显式使用StringBuilder
的concatBuilder()
方法对于java8和java9看起来完全相同:
public java.lang.String concatBuilder();
Code:
0: new #17 // class java/lang/StringBuilder
3: dup
4: invokespecial #18 // Method java/lang/StringBuilder."<init>":()V
7: astore_1
8: iconst_0
9: istore_2
10: iload_2
11: aload_0
12: getfield #19 // Field howmany:I
15: if_icmpge 30
18: aload_1
19: iload_2
20: invokevirtual #20 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
23: pop
24: iinc 2, 1
27: goto 10
30: aload_1
31: invokevirtual #21 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: areturn
注意,StringBuilder.append
的调用发生在循环内部,而StringBuilder.toString
是在循环外部调用的。这一点很重要--这意味着不会创建中间对象。在java8字节码中则略有不同:
Java8中的concatPlain()
方法:
public java.lang.String concatPlain();
Code:
0: ldc #22 // String
2: astore_1
3: iconst_0
4: istore_2
5: iload_2
6: aload_0
7: getfield #19 // Field howmany:I
10: if_icmpge 38
13: new #17 // class java/lang/StringBuilder
16: dup
17: invokespecial #18 // Method java/lang/StringBuilder."<init>":()V
20: aload_1
21: invokevirtual #23 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: iload_2
25: invokevirtual #20 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
28: invokevirtual #21 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
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稍好一些(增加了性能结果):
Method concatPlain()
Java9:
public java.lang.String concatPlain();
Code:
0: ldc #22 // String
2: astore_1
3: iconst_0
4: istore_2
5: iload_2
6: aload_0
7: getfield #19 // Field howmany:I
10: if_icmpge 27
13: aload_1
14: iload_2
15: invokedynamic #23, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;I)Ljava/lang/String;
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:
对于Java9,使用-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 concatPlain():
Java9 concatBuilder():
Java9 concatPlain():
所以,回答你的问题,我可以说,无论是java8还是java9都无法避免在循环中创建中间对象。
更新:
正如@Eugene指出的那样,裸字节码迁移是没有意义的,因为JIT在运行时做了很多优化,这在我看来是合乎逻辑的,所以我决定添加optimized by JIT代码的输出(由-XX:CompileCommand=print,*LoopTest.concatPlain
捕获)。
JAVA 8:
0x00007f8c2d216d29: callq 0x7f8c2d0fdea0 ; OopMap{rsi=Oop [96]=Oop off=1550}
;*synchronization entry
; - org.sample.LoopTest::concatPlain@-1 (line 73)
; {runtime_call}
0x00007f8c2d216d2e: jmpq 0x7f8c2d216786
0x00007f8c2d216d33: mov %rdx,%rdx
0x00007f8c2d216d36: callq 0x7f8c2d0fa1a0 ; OopMap{r9=Oop [96]=Oop off=1563}
;*new ; - org.sample.LoopTest::concatPlain@13 (line 75)
; {runtime_call}
0x00007f8c2d216d3b: jmpq 0x7f8c2d2167e6
0x00007f8c2d216d40: mov %rbx,0x8(%rsp)
0x00007f8c2d216d45: movq $0xffffffffffffffff,(%rsp)
0x00007f8c2d216d4d: callq 0x7f8c2d0fdea0 ; OopMap{r9=Oop [96]=Oop rax=Oop off=1586}
;*synchronization entry
; - java.lang.StringBuilder::<init>@-1 (line 89)
; - org.sample.LoopTest::concatPlain@17 (line 75)
; {runtime_call}
0x00007f8c2d216d52: jmpq 0x7f8c2d21682d
0x00007f8c2d216d57: mov %rbx,0x8(%rsp)
0x00007f8c2d216d5c: movq $0xffffffffffffffff,(%rsp)
0x00007f8c2d216d64: callq 0x7f8c2d0fdea0 ; OopMap{r9=Oop [96]=Oop rax=Oop off=1609}
;*synchronization entry
; - java.lang.AbstractStringBuilder::<init>@-1 (line 67)
; - java.lang.StringBuilder::<init>@3 (line 89)
; - org.sample.LoopTest::concatPlain@17 (line 75)
; {runtime_call}
0x00007f8c2d216d69: jmpq 0x7f8c2d216874
0x00007f8c2d216d6e: mov %rbx,0x8(%rsp)
0x00007f8c2d216d73: movq $0xffffffffffffffff,(%rsp)
0x00007f8c2d216d7b: callq 0x7f8c2d0fdea0 ; OopMap{r9=Oop [96]=Oop rax=Oop off=1632}
;*synchronization entry
; - java.lang.Object::<init>@-1 (line 37)
; - java.lang.AbstractStringBuilder::<init>@1 (line 67)
; - java.lang.StringBuilder::<init>@3 (line 89)
; - org.sample.LoopTest::concatPlain@17 (line 75)
; {runtime_call}
0x00007f8c2d216d80: jmpq 0x7f8c2d2168bb
0x00007f8c2d216d85: callq 0x7f8c2d0faa60 ; OopMap{r9=Oop [96]=Oop r13=Oop off=1642}
;*newarray
; - java.lang.AbstractStringBuilder::<init>@6 (line 68)
; - java.lang.StringBuilder::<init>@3 (line 89)
; - org.sample.LoopTest::concatPlain@17 (line 75)
; {runtime_call}
0x00007f8c2d216d8a: jmpq 0x7f8c2d21693a
0x00007f8c2d216d8f: mov %rdx,0x8(%rsp)
0x00007f8c2d216d94: movq $0xffffffffffffffff,(%rsp)
0x00007f8c2d216d9c: callq 0x7f8c2d0fdea0 ; OopMap{r9=Oop [96]=Oop r13=Oop off=1665}
;*synchronization entry
; - java.lang.StringBuilder::append@-1 (line 136)
; - org.sample.LoopTest::concatPlain@21 (line 75)
; {runtime_call}
0x00007f8c2d216da1: jmpq 0x7f8c2d216a1c
0x00007f8c2d216da6: mov %rdx,0x8(%rsp)
0x00007f8c2d216dab: movq $0xffffffffffffffff,(%rsp)
0x00007f8c2d216db3: callq 0x7f8c2d0fdea0 ; OopMap{[80]=Oop [96]=Oop off=1688}
;*synchronization entry
; - java.lang.StringBuilder::append@-1 (line 208)
; - org.sample.LoopTest::concatPlain@25 (line 75)
; {runtime_call}
0x00007f8c2d216db8: jmpq 0x7f8c2d216b08
0x00007f8c2d216dbd: mov %rdx,0x8(%rsp)
0x00007f8c2d216dc2: movq $0xffffffffffffffff,(%rsp)
0x00007f8c2d216dca: callq 0x7f8c2d0fdea0 ; OopMap{[80]=Oop [96]=Oop off=1711}
;*synchronization entry
; - java.lang.StringBuilder::toString@-1 (line 407)
; - org.sample.LoopTest::concatPlain@28 (line 75)
; {runtime_call}
0x00007f8c2d216dcf: jmpq 0x7f8c2d216bf8
0x00007f8c2d216dd4: mov %rdx,%rdx
0x00007f8c2d216dd7: callq 0x7f8c2d0fa1a0 ; OopMap{[80]=Oop [96]=Oop off=1724}
;*new ; - java.lang.StringBuilder::toString@0 (line 407)
; - org.sample.LoopTest::concatPlain@28 (line 75)
; {runtime_call}
0x00007f8c2d216ddc: jmpq 0x7f8c2d216c39
0x00007f8c2d216de1: mov %rax,0x8(%rsp)
0x00007f8c2d216de6: movq $0x23,(%rsp)
0x00007f8c2d216dee: callq 0x7f8c2d0fdea0 ; OopMap{[96]=Oop [104]=Oop off=1747}
;*goto
; - org.sample.LoopTest::concatPlain@35 (line 74)
; {runtime_call}
0x00007f8c2d216df3: jmpq 0x7f8c2d216cae
正如您所看到的,StringBuilder::toString
是在goto之前调用的,这意味着一切都发生在循环中。与java9 - StringConcatHelper::newString
类似的情况在goto命令之前调用。
JAVA 9:
0x00007fa1256548a4: mov %ebx,%r13d
0x00007fa1256548a7: sub 0xc(%rsp),%r13d ;*isub {reexecute=0 rethrow=0 return_oop=0}
; - java.lang.StringConcatHelper::prepend@5 (line 329)
; - java.lang.invoke.DirectMethodHandle$Holder::invokeStatic@16
; - java.lang.invoke.LambdaForm$BMH/127835623::reinvoke@172
; - java.lang.invoke.LambdaForm$MH/1587176117::linkToTargetMethod@6
; - org.sample.LoopTest::concatPlain@15 (line 75)
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 ;*invokestatic arraycopy {reexecute=0 rethrow=0 return_oop=0}
; - java.lang.String::getBytes@22 (line 2993)
; - java.lang.StringConcatHelper::prepend@11 (line 330)
; - java.lang.invoke.DirectMethodHandle$Holder::invokeStatic@16
; - java.lang.invoke.LambdaForm$BMH/127835623::reinvoke@172
; - java.lang.invoke.LambdaForm$MH/1587176117::linkToTargetMethod@6
; - org.sample.LoopTest::concatPlain@15 (line 75)
0x00007fa1256548c9: test %r9d,%r9d
0x00007fa1256548cc: jbe 0x7fa1256548ef
0x00007fa1256548ce: movsxd %r9d,%rdx
0x00007fa1256548d1: lea (%r12,%r8,8),%r10 ;*getfield value {reexecute=0 rethrow=0 return_oop=0}
; - java.lang.String::length@1 (line 669)
; - java.lang.StringConcatHelper::mixLen@2 (line 116)
; - java.lang.invoke.DirectMethodHandle$Holder::invokeStatic@11
; - java.lang.invoke.LambdaForm$BMH/127835623::reinvoke@105
; - java.lang.invoke.LambdaForm$MH/1587176117::linkToTargetMethod@6
; - org.sample.LoopTest::concatPlain@15 (line 75)
0x00007fa1256548d5: lea 0x10(%r12,%r8,8),%rdi
0x00007fa1256548da: mov %rcx,%r10
0x00007fa1256548dd: lea 0x10(%rcx,%r13),%rsi
0x00007fa1256548e2: movabs $0x7fa11db9d640,%r10
0x00007fa1256548ec: callq %r10 ;*invokestatic arraycopy {reexecute=0 rethrow=0 return_oop=0}
; - java.lang.String::getBytes@22 (line 2993)
; - java.lang.StringConcatHelper::prepend@11 (line 330)
; - java.lang.invoke.DirectMethodHandle$Holder::invokeStatic@16
; - java.lang.invoke.LambdaForm$BMH/127835623::reinvoke@172
; - java.lang.invoke.LambdaForm$MH/1587176117::linkToTargetMethod@6
; - org.sample.LoopTest::concatPlain@15 (line 75)
0x00007fa1256548ef: cmp 0xc(%rsp),%ebx
0x00007fa1256548f3: jne 0x7fa125654cb9 ;*ifeq {reexecute=0 rethrow=0 return_oop=0}
; - java.lang.StringConcatHelper::newString@1 (line 343)
; - java.lang.invoke.DirectMethodHandle$Holder::invokeStatic@14
; - java.lang.invoke.LambdaForm$BMH/127835623::reinvoke@194
; - java.lang.invoke.LambdaForm$MH/1587176117::linkToTargetMethod@6
; - org.sample.LoopTest::concatPlain@15 (line 75)
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) ; {metadata('java/lang/String')}
0x00007fa125654930: mov %r12d,0xc(%rax)
0x00007fa125654934: mov %r12,0x10(%rax) ;*new {reexecute=0 rethrow=0 return_oop=0}
; - java.lang.StringConcatHelper::newString@36 (line 346)
; - java.lang.invoke.DirectMethodHandle$Holder::invokeStatic@14
; - java.lang.invoke.LambdaForm$BMH/127835623::reinvoke@194
; - java.lang.invoke.LambdaForm$MH/1587176117::linkToTargetMethod@6
; - org.sample.LoopTest::concatPlain@15 (line 75)
0x00007fa125654938: mov 0x30(%rsp),%r10
0x00007fa12565493d: shr $0x3,%r10
0x00007fa125654941: mov %r10d,0xc(%rax) ;*synchronization entry
; - java.lang.StringConcatHelper::newString@-1 (line 343)
; - java.lang.invoke.DirectMethodHandle$Holder::invokeStatic@14
; - java.lang.invoke.LambdaForm$BMH/127835623::reinvoke@194
; - java.lang.invoke.LambdaForm$MH/1587176117::linkToTargetMethod@6
; - org.sample.LoopTest::concatPlain@15 (line 75)
0x00007fa125654945: mov 0x8(%rsp),%ebx
0x00007fa125654949: incl %ebx ; ImmutableOopMap{rax=Oop [0]=Oop }
;*goto {reexecute=1 rethrow=0 return_oop=0}
; - org.sample.LoopTest::concatPlain@24 (line 74)
0x00007fa12565494b: test %eax,0x1a8996af(%rip) ;*goto {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.LoopTest::concatPlain@24 (line 74)
; {poll}
发布于 2018-03-24 05:49:33
您的循环每次都会创建一个新的字符串。StringBuilder (而不是StringBuffer,它是同步的,不应该使用)避免每次实例化一个新对象。
Java 9可能会添加新特性,但如果情况发生了变化,我会感到惊讶。这个问题比Java 8早得多。
添加:
Java 9修改了在单个语句中使用"+“运算符时执行字符串连接的方式。在Java 8之前,它使用的是构建器。现在,它使用了一种更有效的方法。然而,这并没有解决在循环中使用"+=“的问题。
https://stackoverflow.com/questions/49458564
复制相似问题