DEV Community

Cover image for Do you know how to evaluate performance of your Java applications?
Anderson Bosa
Anderson Bosa

Posted on

2

Do you know how to evaluate performance of your Java applications?

Recently, while chatting with an Expert here at MELI (Mercado Livre), I asked more about his past experiences, curious about the problems he’s tackled. That’s when he shared a particular scenario with me that piqued my interest: the impact of boxing and unboxing in poorly implemented algorithms. I won’t share his real case due to NDA reasons. However, I went off to study this topic myself, and now I’m bringing what I’ve learned in this post for you all.

If you work with Java, you’ve dealt with collections and autoboxing, but are you aware of the hidden cost of these automatic conversions? I hope you enjoy the content, and all feedback is welcome—thanks!

First, What Are Boxing and Unboxing?

In Java, boxing is the automatic conversion of a primitive type (e.g., int) into its wrapper equivalent (e.g., Integer). Unboxing is the reverse process. This has been around since Java 5 with autoboxing, making life easier when using APIs like ArrayList, which only accept objects. Here’s a basic example:

int primitive = 42;  
Integer wrapper = primitive; // Autoboxing  
int backAgain = wrapper;     // Autounboxing  
Enter fullscreen mode Exit fullscreen mode

It seems simple, but the problem arises when these conversions happen on a large scale or in poorly designed algorithms.

Some Terms That Came Up

The Heap is the memory area where Java allocates objects and arrays. It’s a global memory space, accessible by different threads in the application.

The Stack is a memory area used to store local variables (primitives or object references) and method execution details (parameters, scope variables, return address).

The Garbage Collector, meanwhile, is the JVM’s automatic janitor. It’s responsible for finding objects in the Heap that no longer have active references (nobody’s pointing to them anymore) and freeing up their memory for reuse.

Why Does This Affect Performance?

Primitive types are stored in the stack and are super lightweight—an int takes up 4 bytes. An Integer, on the other hand, is an object in the heap, with metadata overhead (it can reach 16 bytes or more, depending on the JVM). Each boxing creates a new object, increasing memory usage and the garbage collector’s workload. In loops or massive operations, this cost piles up fast.

Hands-On

Comparing Boxing vs. Primitives. Let’s test this with real code. The goal here is to sum 10 million integers in two ways:

  1. Using an ArrayList (with boxing)
  2. Using an int array.

GitHub Repository: https://github.com/andersonbosa/boxing-performance-test/blob/main/src/BoxingPerformanceTest.java

I kept it in the repo for better readability.

The results on your computer might vary slightly from those below (since computational power differs between machines):

Time with array (primitive): 29 ms  
Memory used by primitive array: 0 MB  
----------------------------------------  
Time with ArrayList (boxing): 171 ms  
Memory used by ArrayList: 288 MB  
Enter fullscreen mode Exit fullscreen mode

Why? With the ArrayList, every int becomes an Integer (10 million objects!), while the array uses just contiguous memory (TL;DR: fast access, slow insertion) for primitives. The garbage collector also has to clean up those objects afterward, adding to the impact.

Sure, you might be thinking you’re not about to go summing 10 million numbers, right? So let’s move on—I’ll show the impact of boxing/unboxing in poorly thought-out real-world algorithms.

Spoiler

Try setting the size to 500_000_000 to see the consequences!

VisualVM + IntelliJ Plugin

Beware of Poorly Implemented Algorithms

Nested loops (a loop inside a loop) handling request data, authentication operations, or hash calculations processing large input volumes—what do they have in common? The memory and CPU overhead can easily scale, turning functional code into a bottleneck. Here are some practical examples I picked up from my research and chats with colleagues who’ve faced these issues in real systems:

  1. Authentication: Batch ID Verification

A REST API validates user IDs from a JWT against an authorized list.

import java.util.List;  
import java.util.Set;  
import java.util.HashSet;  

public class AuthService {  
    private static final Set<Integer> AUTHORIZED = new HashSet<>(List.of(1001, 1002, 1003));  

    public boolean validateIds(List<Integer> userIds) {  
        for (Integer id : userIds) { // Unboxing  
            if (!AUTHORIZED.contains(id)) { // More unboxing  
                return false;  
            }  
        }  
        return true;  
    }  
}  
Enter fullscreen mode Exit fullscreen mode

For a request with thousands of IDs (e.g., batch validation), each Integer requires unboxing. The alternative? Use int[] and IntHashSet (from the FastUtil library).

  1. Request Processing: Status Filtering

Imagine an endpoint that filters HTTP status codes (e.g., 200, 404, etc.) from received logs.

import java.util.ArrayList;  
import java.util.List;  

public class LogProcessor {  
    public List<Integer> filterStatus(List<Integer> statusCodes) {  
        List<Integer> filtered = new ArrayList<>();  
        for (Integer code : statusCodes) { // Unboxing  
            if (code >= 200 && code < 300) { // More unboxing  
                filtered.add(code); // Boxing  
            }  
        }  
        return filtered;  
    }  
}  
Enter fullscreen mode Exit fullscreen mode

With thousands of requests, each filtering operation generating boxing/unboxing would degrade performance. Using int[] would solve the issue.

  1. Encryption/Hashing: Data Integrity Validation

For example, a web service calculates and compares SHA-256 hashes of file chunks sent via request to verify integrity (e.g., in a chunked upload).

import java.security.MessageDigest;  
import java.util.ArrayList;  
import java.util.List;  

public class HashValidator {  
    public List<Integer> calculateHashBytes(byte[] chunk) throws Exception {  
        MessageDigest md = MessageDigest.getInstance("SHA-256");  
        byte[] hash = md.digest(chunk);  
        List<Integer> hashValues = new ArrayList<>();  
        for (byte b : hash) {  
            hashValues.add((int) b); // Boxing to Integer  
        }  
        return hashValues; // Returns for comparison  
    }  
}  
Enter fullscreen mode Exit fullscreen mode

How could we improve this? Each byte of the hash (32 bytes for SHA-256) is converted into an Integer, creating 32 objects per call. I simulated this with 10,000 1 KB chunks: boxing increased memory usage by 20% and added overhead to the garbage collector, impacting endpoint latency. Alternative? Stick with byte[] or use int[] if conversion is needed.

Takeaways

  • Prefer primitives: If you don’t need objects, use int, double, etc. Arrays like int[] are your friends in intensive operations.
  • Avoid boxing in loops: Each iteration with autoboxing means one more object in the heap.
  • Know your alternatives! Libraries like Trove or Eclipse Collections offer optimized collections for primitives.
  • Profile your code: Tools like VisualVM or JProfiler reveal where boxing is costing you.

My Conclusion

Boxing and unboxing are handy mechanisms, but their indiscriminate use can severely compromise performance, especially in critical or poorly tested code sections. As developers and engineers, our role goes beyond just making code work. We should aim for solutions that not only solve the problem but are also efficient, resource-sustainable, and resilient to failures.

Test, compare, and optimize—your code, your users, and your career will thank you (lol).

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (4)

Collapse
 
zethix profile image
Andrey Rusev

Good job! :)

In my opinion there's a lot of ... what I call collections abuse - the tendency to solve everything with a List and a Map... which hurts in so many ways (including as you say - with boxing/unboxing)... Anyway, I'm just saying that ... if you're looking to write more on such topics - there's a lot! :)

Collapse
 
andersonbosa profile image
Anderson Bosa

I appreciate your words, thank you @zethix ;)

Could you suggest me some subject of study? I'm interested in performance and troubleshooting with Java or Golang but I'm without a north for now...

Collapse
 
zethix profile image
Andrey Rusev

Pfeww... there's a lot... I don't know, just of the top of my head (and because it also seems related to your post):

  • If you 'stick to arrays' - you can take a look at the JIT compiler, and in particular - how it turns java code into native... and how when you're using arrays - it can even 'vectorize' it - that is - will optimize it to use SIMD instructions - if available on the CPU

  • the stack vs heap (and how that affects GC) topic is also a really good one I think, quite a lot of things can be done specifically by choosing when to use the stack...

I'll think of some more, but, until then - take a look at the above, let me know if they look interesting to you... Or if you need some more info...

Thread Thread
 
andersonbosa profile image
Anderson Bosa

Awesome! I will start with how stack vs heap can affect the GC, but I was very interested in the term SIMD instructions, that one is new to me. I'll send you updates when I have something new to share ;) Thanks for guiding me!

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more