This issue is specific to Java 11 “target” class files; if you target to Java 8 class files (but still run with Java 11), there’s no slowdown. I changed the test to be standalone to make it easy to run. The following is on desktop with Oracle-distributed OpenJDK 11.0.1.
javac Slow3.java (or javac -source 1.8 Slow3.java), java Slow3 result:
Don't slow me down, m_a: 1.0, m_b: 2.0, m_c: 3.0, m_d: 4.0, m_e: 5.0, m_f: 6.0, m_g: 7.0, m_h: 8.0, m_i: 9.0
Slow me down, m_a: 1.0, m_b: 2.0, m_b: 3.0, m_b: 4.0, m_b: 5.0, m_b: 6.0, m_b: 7.0, m_b: 8.0, m_b: 9.0
Slow me down, m_a: 1.0, m_b: 2.0, m_b: 3.0, m_b: 4.0, m_b: 5.0, m_b: 6.0, m_b: 7.0, m_b: 8.0, m_b: 9.0
Slow me down, m_a: 1.0, m_b: 2.0, m_b: 2.0, m_b: 4.0, m_b: 5.5, m_b: 4.2, m_b: 7.0, m_b: 8.0, m_b: 9.0
fast time: 9.999275207519531E-4
slow time: 0.06799983978271484
slow time (again): 0.0
slow time (after changing): 0.0
javac -source 1.8 -target 1.8 Slow3.java, java Slow3 result:
Don't slow me down, m_a: 1.0, m_b: 2.0, m_c: 3.0, m_d: 4.0, m_e: 5.0, m_f: 6.0, m_g: 7.0, m_h: 8.0, m_i: 9.0
Slow me down, m_a: 1.0, m_b: 2.0, m_b: 3.0, m_b: 4.0, m_b: 5.0, m_b: 6.0, m_b: 7.0, m_b: 8.0, m_b: 9.0
Slow me down, m_a: 1.0, m_b: 2.0, m_b: 3.0, m_b: 4.0, m_b: 5.0, m_b: 6.0, m_b: 7.0, m_b: 8.0, m_b: 9.0
Slow me down, m_a: 1.0, m_b: 2.0, m_b: 2.0, m_b: 4.0, m_b: 5.5, m_b: 4.2, m_b: 7.0, m_b: 8.0, m_b: 9.0
fast time: 0.0010001659393310547
slow time: 0.0
slow time (again): 0.0
slow time (after changing): 0.0
Disassembling the two class files shows the difference.
With Java 8 target class file, the compiler turns the +'s into a StringBuilder and builds the string that way.
With Java 11 target class file, the compiler makes a single InvokeDynamic call to a function called makeConcatWithConstants.
My guess is the first time InvokeDynamic is called on this function, a lot of work has to be done to create an optimized function. Later calls are much faster because the function has already been created. I’m challenged to call this a “bug” in the typical sense. It’s certainly a lot slower on first invocation, but it’s likely a lot faster on later invocations. There’s lots of cases of this throughout Java. E.g. the first time you use a function it may need to load a new .class file and that can take a lot of time. This does seem to be a particularly extreme case.
If you really care about this, you can change the string concatenation method with a parameter to the java runtime.
“java -Djava.lang.invoke.stringConcat=BC_SB Slow3” with the Java 11 class file results in:
Don't slow me down, m_a: 1.0, m_b: 2.0, m_c: 3.0, m_d: 4.0, m_e: 5.0, m_f: 6.0, m_g: 7.0, m_h: 8.0, m_i: 9.0
Slow me down, m_a: 1.0, m_b: 2.0, m_b: 3.0, m_b: 4.0, m_b: 5.0, m_b: 6.0, m_b: 7.0, m_b: 8.0, m_b: 9.0
Slow me down, m_a: 1.0, m_b: 2.0, m_b: 3.0, m_b: 4.0, m_b: 5.0, m_b: 6.0, m_b: 7.0, m_b: 8.0, m_b: 9.0
Slow me down, m_a: 1.0, m_b: 2.0, m_b: 2.0, m_b: 4.0, m_b: 5.5, m_b: 4.2, m_b: 7.0, m_b: 8.0, m_b: 9.0
fast time: 0.0019998550415039062
slow time: 0.003000020980834961
slow time (again): 0.0
slow time (after changing): 0.0
Some more information on this can be found on Stack Overflow: https://stackoverflow.com/questions/46512888/how-is-string-concatenation-implemented-in-java-9