My question is: how many objects are created in this loop? Are there any intermediate objects? How can I check this?
Spoiler:
The JVM does not attempt to skip intermediate objects in a loop - therefore, they will be created using simple concatenation.
First, take a look at the bytecode. I used the performance tests kindly provided by @Eugene, compiled them for java8 and then for java9. These are the two methods that we are going to compare:
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; }
My java versions are as follows:
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 Version 1.20
Here is the result I get from javap -c LoopTest.class :
The concatBuilder() method, which uses StringBuilder , clearly looks exactly the same for java8 and java9:
public java.lang.String concatBuilder(); Code: 0: new #17
Note that calling the StringBuilder.append loop occurs inside the loop, and StringBuilder.toString is called outside of the loop. This is important - this means that no intermediate objects will be created. In java8, the bytecode is a bit different:
The concatPlain() method in Java8:
public java.lang.String concatPlain(); Code: 0: ldc #22
You can see that in java8 StringBuilder.append and StringBuilder.toString called inside the loop StringBuilder.toString , which means that it is not even trying to omit the creation of intermediate objects! . This can be described in the code below:
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; }
This explains the performance difference between concatPlain() and concatBuilder() (which is several thousand times (!)). The same problem occurs with java9 - it does not try to avoid intermediate objects inside the loop, but it does a little better work inside the loop than java8 (performance results are added):
concatPlain() method of Java9:
public java.lang.String concatPlain(); Code: 0: ldc #22
Here are the performance results:
JAVA 8:
JAVA 9:
For java 9, there are different strategies defined with -Djava.lang.invoke.stringConcat . I tried them all:
By default ( MH_INLINE_SIZED_EXACT ):
-Djava.lang.invoke.stringConcat = BC_SB
-Djava.lang.invoke.stringConcat = BC_SB_SIZED
-Djava.lang.invoke.stringConcat = BC_SB_SIZED_EXACT
-Djava.lang.invoke.stringConcat = BC_SB_SIZED_EXACT
-Djava.lang.invoke.stringConcat = MH_SB_SIZED_EXACT
-Djava.lang.invoke.stringConcat = MH_INLINE_SIZED_EXACT (yes, this is the default, but I decided to set it explicitly for clarity of the experiment)
I decided to research memory usage, but did not find anything interesting except that java9 consumes more memory. Attached screenshots in case someone is interested. Of course, they were made after actual performance measurements, but not during them.
Java8 concatBuilder ():
Java8 concatPlain ():
Java9 concatBuilder ():
Java9 concatPlain (): 
So yes, in answering your question, I can say that neither java8 nor java9 can create intermediate objects inside the loop.
UPDATE:
As @Eugene noted, bare migt bytecode does not make sense, since JIT does a lot of optimizations at runtime, which seems logical to me, so I decided to add the optimization result using JIT code (shot with -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
As you can see, StringBuilder::toString is called before goto, which means that everything happens inside the loop. A similar situation with java9 - StringConcatHelper::newString is called before the goto command.
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}