Performance cost of autoboxing Java primitive types explained

When I was working through a few of Marcus Hirt’s Java Flight Recorder tutorials, I was surprised by how much the JVM performance cost is affected by the boxing and unboxing of Java primitive types.

As every Java developer knows, there are eight primitive types in Java:

  1. byte me = 5;
  2. short circuit = 10;
  3. int eresting = 30;
  4. long johns = 22;
  5. float sum = 41;
  6. double trouble = 99;
  7. char broiled = ‘a’;
  8. boolean flag = true;

Primitive types represent the simplest, most direct way to represent data in code. Even the most complicated classes in Java can be flattened down to nothing more than the set of primitive data types they represent. But primitive types aren’t objects, and that presents a problem.

For example, all the collection classes in the JDK hold data as objects. If a developer has a set of int values they want to store in an ArrayList, it can’t be done. Unless, of course, they use a corresponding wrapper class or take advantage the autoboxing in Java feature.

Your intro to GitHub Actions training course

Here’s how to get started with GitHub Actions:

Follow these tutorials and you’ll learn GitHub Actions fast.

Examples of autoboxing in Java

For every primitive type, there is a corresponding wrapper class:

Historically, if developers wanted to store a set of doubles in a collection class, they’d have to convert from the primitive type to the wrapper class:

int x = 10;
ArrayList<E> list = new ArrayList();
// list.add(10); Pre JDK 1.5 autoboxing would not work
Integer wrapper = Integer.valueOf(x);
list.add(wrapper);

However, JDK version 1.5 introduced a feature known as the autoboxing of Java primitive types. It means that on newer JDKs, a wrapper class will be created automatically when a primitive type is used anywhere a reference type is expected. As such, on post JDK 1.5 versions of the JVM, the use-case above won’t need to use a wrapper class. The Java boxing and autoboxing of primitive types will take care of it for you:

int x = 10;
ArrayList<E> list = new ArrayList();
list.add(10); // This is primitive type autoboxing in Java 
//Integer wrapper = Integer.valueOf(x);
//list.add(wrapper);

How autoboxing Java hurts performance

I always assumed that when Java introduced primitive type boxing and unboxing, JVM level optimizations to address any Java autoboxing related performance issues were implemented as well. I assumed that moving between wrapper classes and primitive types was a relatively smooth operation when it came to clock cycles, garbage collection and memory consumption.

I couldn’t have been more wrong.

Here’s the highly contrived use case, which is largely inspired by Marcus Hirt’s JMC examples.

First, we create a simple component that acts as a wrapper class for an int value. The class is named SnoopInt:

package com.mcnz.jfr.jmc;
public final class SnoopInt {
  final int id;
  SnoopInt(int id) { this.id = id; }
  int getId() { return id; }
}

The Java autoboxing class

We then have a runnable class that shoves one million primitive type int values into a map.

Then, we make a copy of all the values in the map and go through the original map to confirm that all the values in the copy are also in the original. It’s a contrived example, but it puts a load on the JVM and it produces some interesting results in terms of garbage collection and memory performance metrics.

There’s a great deal of autoboxing Java primitive types involved, so I named the class MikeTyson:

package com.mcnz.jfr.jmc;
import java.util.*;

public final class MikeTyson implements Runnable {
  private final Map<Integer, SnoopInt> map = new HashMap<>();
  public MikeTyson() {
    for (int i = 0; i < 1_000_000; i++) {
      map.put(i, new SnoopInt(i));
    }
  }
 
  public void run() {
    long yieldCounter = 0;
    while (true) {
      Collection copyOfValues = map.values();
      for (SnoopInt snoopIntCopy : copyOfValues) {
        if (!map.containsKey(snoopIntCopy.getId()))
          System.out.println("Now this is strange!");
        if (++yieldCounter % 1000 == 0)
          System.out.println("Boxing and unboxing");
          Thread.yield();
      }
    }
  }
  
  public static void main(String[] args) throws java.io.IOException {
    ThreadGroup threadGroup = new ThreadGroup("Workers");
    Thread[] threads = new Thread[8];
    for (int i = 0; i < threads.length; i++) {
      threads[i] = new Thread(threadGroup, new MikeTyson(), "Allocator Thread " + i);
      threads[i].setDaemon(true);
      threads[i].start();
    }
    System.out.print("Press  to quit!");
    System.out.flush();
    System.in.read();
  }
}

Autoboxing and Java Mission Control

A quick profiling of this program with Java Flight Recorder and the JDK Mission Control Eclipse Plugin triggers a red warning flagging Primitive to Object Conversions as being problematic. The autoboxing is causing a performance problem.

java autoboxing performance

Boxing and unboxing of Java primitive types causes JVM performance issues.

Furthermore, when you inspect Java Mission Control’s garbage collection metrics, you’ll see that garbage collection is off the charts:

Java Mission Control Garbage GC

Java Mission Control shows rampant garbage collection routines hurting performance when autoboxing is used.

Fix autoboxing in Java

How do you solve the Java autoboxing performance problem?

Developers can fix the problem by changing only a few lines of code. If the Integer reference type is used throughout the application, all the garbage collection problems will go away.

The Java constructor of the MikeTyson class gets a small change:

public MikeTyson() {
  for (int i = 0; i < 700_000; i++) {
    map.put(Integer.valueOf(i), 
              new SnoopInt(Integer.valueOf(i)));
  }
}

And the custom wrapper class gets updated to use the Integer reference type as well:

public final class SnoopInt {
  final Integer id;
  SnoopInt(Integer id) { this.id = id; }
  Integer getId() { return id; }
}

Monitor and profile with JFR and JMC

After those small changes are made and you start Java Flight Recorder again, the Java primitive type boxing and unboxing performance issues go away. There’s no discernable increase in garbage collection, and Java Mission Control won’t report any primitive to object conversion issues after the Java Flight Recorder runs.

I always assumed that the performance implications of autoboxing Java was minor, but again, I was wrong. The performance implications can be significant.

That’s why it’s important to constantly profile your applications with tools like Java’s JVM Flight Recorder and the JDK Mission Control tool. Assumptions aren’t bad, so long as a mechanism exists to validate them, as this Java autoboxing performance example clearly demonstrates.

 

Learn Apache Maven tutorial