Yes, HotSpot JIT can eliminate redundant allocations in a local context.
This optimization is provided by the Escape Analysis enabled since JDK 6u23. It is often confused with on-stack allocation, but in fact it is much more powerful, since it allows not only to allocate objects on stack, but to eliminate allocation altogether by replacing object fields with variables (Scalar Replacement) that are subject to further optimizations.
The optimization is controlled by -XX:+EliminateAllocations
JVM option which is ON by default.
Thanks to allocation elimination optimization, both your examples of creating a Message
object work effectively the same way. They do not allocate intermediate objects; just the final one.
Your benchmark shows misleading results, because it collects many common pitfalls of microbenchmarking:
- it incorporates several benchmarks in a single method;
- it measures an OSR stub instead of the final compiled version;
- it does not do warm-up iterations;
- it does not consume results, etc.
Let’s measure it correctly with JMH. As a bonus, JMH has the allocation profiler (-prof gc
) which shows how many bytes are really allocated per iteration. I’ve added the third test that runs with EliminateAllocations
optimization disabled to show the difference.
package bench;
import org.openjdk.jmh.annotations.*;
@State(Scope.Benchmark)
public class MessageBench {
@Benchmark
public Message builder() {
return Message.createNew()
.email(getString())
.assignee(getString())
.conversationId(getString())
.escalationEmail(getString())
.subject(getString())
.userId(getString())
.create();
}
@Benchmark
public Message immutable() {
return new Message()
.email(getString())
.assignee(getString())
.conversationId(getString())
.escalationEmail(getString())
.subject(getString())
.userId(getString());
}
@Benchmark
@Fork(jvmArgs = "-XX:-EliminateAllocations")
public Message immutableNoOpt() {
return new Message()
.email(getString())
.assignee(getString())
.conversationId(getString())
.escalationEmail(getString())
.subject(getString())
.userId(getString());
}
private String getString() {
return "hello";
}
}
Here are the results. Both builder
and immutable
perform equally and allocate just 40 bytes per iteration (exactly the size of one Message
object).
Benchmark Mode Cnt Score Error Units
MessageBench.builder avgt 10 6,232 ± 0,111 ns/op
MessageBench.immutable avgt 10 6,213 ± 0,087 ns/op
MessageBench.immutableNoOpt avgt 10 41,660 ± 2,466 ns/op
MessageBench.builder:·gc.alloc.rate.norm avgt 10 40,000 ± 0,001 B/op
MessageBench.immutable:·gc.alloc.rate.norm avgt 10 40,000 ± 0,001 B/op
MessageBench.immutableNoOpt:·gc.alloc.rate.norm avgt 10 280,000 ± 0,001 B/op