Will the jit optimize new objects

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

Leave a Comment