First you'll have to learn a lot about the internals of HotSpot, in particular the fact that your code is first interpreted, then at a certain point compiled into native code.
A lot of optimizations happen while compiling, based on results of both static and dynamic analysis of your code.
Specifically, in your code,
String s = "test";
is a clear no-op. The compiler will emit no code whatsoever for this line. All that remains is the loop itself, and the whole loop may be eliminated if HotSpot proves it has no observable outside effects.
Second, even the code
String s = new String("test");
may result in almost the same thing as above because it is very easy to prove that your new String
is an instance which cannot escape from the method where it is created.
With your code, the measurements are mixing up the performance of interpreted bytecode, the delay it takes to compile the code and swap it in by On-Stack Replacement, and then the performance of the native code.
Basically, the measurements you are making are measuring everything but the effect you have set out to measure.
To make the arguments more solid, I have repeated the test with jmh
:
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 1, time = 1)
@Measurement(iterations = 3, time = 1)
@Threads(1)
@Fork(2)
public class Strings
{
static final int ITERS = 1_000;
@GenerateMicroBenchmark
public void literal() {
for (int i = 0; i < ITERS; i++) { String s = "test"; }
}
@GenerateMicroBenchmark
public void newString() {
for (int i = 0; i < ITERS; i++) { String s = new String("test"); }
}
}
and these are the results:
Benchmark Mode Samples Mean Mean error Units
literal avgt 6 0.625 0.023 ns/op
newString avgt 6 43.778 3.283 ns/op
You can see that the whole method body is eliminated in the case of the string literal, while with new String
the loop remains, but nothing in it because the time per loop iteration is just 0.04 nanoseconds. Definitely no String
instances are allocated.