Java Exception Handling

Java Exception Handling – OutOfMemoryError

Making our way through our detailed Java Exception Handling series, today we’ll be going over the OutOfMemoryError, which is thrown when the Java Virtual Machine (JVM) is unable to allocate an object due to lack of memory.

In this article we’ll explore the OutOfMemoryError in more detail, starting with where it sits in the larger Java Exception Hierarchy. We’ll then take a look at some fully-functional Java code samples that will illustrate how memory allocation works, and how improper memory management might lead to OutOfMemoryErrors in your own code, so let’s get started!

The Technical Rundown

All Java errors implement the java.lang.Throwable interface, or are extended from another inherited class therein. The full exception hierarchy of this error is:

Full Code Sample

Below is the full code sample we’ll be using in this article. It can be copied and pasted if you’d like to play with the code yourself and see how everything works.

When Should You Use It?

To understand why an OutOfMemoryError might occur, we must first briefly examine Java’s memory management scheme. When the Java Virtual Machine (JVM) first launches, it sets aside a chunk of heap memory (commonly referred to simply as the heap). A heap is an area of memory that the JVM can use to store newly allocated objects. Any object within the heap that is referenced by another object is considered active, which forces that object to remain in the heap for the duration if its lifespan (i.e. while it remains referenced). Once an object is no longer referenced it is considered garbage, and the garbage collector will reclaim the memory that the object had previously required.

The size of the heap depends on two factors, which can be controlled by commmand-line options when launching Java. -Xms is used to set the initial heap size, which is the initial amount of memory the application allocates to the heap. The -Xmx flag is used to set the maximum heap size, which, as the name suggests, specifies just how many bytes the heap is allocated in total. In short, the amount of heap memory given to the application will impact how many objects, and of what size, can be allocated at once before running into issues.

In addition to heap memory, the JVM also manages another set of memory called non-heap memory. As the name suggests, this is memory that isn’t part of the heap used to store objects, but, instead, is used to store class structures pools, field data, method data, and the executing code of said methods and constructors. This non-heap memory pool can be adjusted, but is initially created when the JVM starts.

Now that we know a bit more about how memory limitations in Java are set and attributed, let’s take a look at some sample code. We’ll start with the allocateMemory(long bytes) method, which holds the majority of the logic that we’ll be using in this example:

The allocateMemory(long bytes) method attempts to allocate memory equivalent to the number of passed bytes. We do so by initializing byte[] arrays of equal size to the number of bytes. However, since Java limits the size of arrays to slightly under the maximum size of an Integer, if the passed bytes parameter exceeds this maximum, we need to create a series of arrays to properly allocate all memory. The MAX_ARRAY_SIZE constant defines the largest array size we can use:

With that, we determine how many “chunks” (maximized arrays) we can fit within the bytes parameter by passing it to the getArrayChunkCount(long bytes) method:

Once determined, we create a two-dimensional array with the first dimension size equal to the number of chunks, and the second dimension equal to MAX_ARRAY_SIZE. Finally, we determine the remainder after chunking and allocate that to its own single-dimension array.

If allocation was successful (i.e. no errors occurred), we output a success message to the log. Otherwise, we output a failed message and the expected OutOfMemoryError.

During processing, we also call the printMemoryUsage() method, which uses the ManagementFactory class to retrieve heap and non-heap memory usage data:

With the help of the formatNumber(long number) method, we’re able to cleanly output the current memory usage during each memory allocation attempt.

Alright, with everything setup we can test this out by intentionally allocating various amounts of memory. Our Main.main(String[] args) method does so by passing increasing numeric values to allocateMemory(long bytes):

This produces the following output, showing the attempt to allocate, the current memory usage stats, and the success message:

Even with a small allocation amount of 24,601, we can see some interesting data from the memory usage stats. Particularly, notice that the heap memory used is only about 4 MB, which is even less than the non-heap memory used to store the application code.

Let’s bump it up a bit to 10,000,000:

Nothing else has really changed except for the heap memory used, so it can accommodate the 10 MB we’ve allocated.

Here we’ll try the maximum Integer size, along with a long value that is an order of magnitude larger than that:

Now we’re starting to really see things ramp up by using over 2 GB of memory. Even the heap memory committed quantity has to be increased, which is essentially the combined amount of working heap memory, plus current memory stored in garbage collection. Thus, the committed amount will (almost) always exceed the actual used heap memory value.

Finally, let’s try somewhere around 3 GB and an excessively large 10 PB to see what happens:

The 3 GB allocation finally puts us over the top and throws an OutOfMemoryError, indicating that the Java heap space has been exceeded. This indicates that our total memory usage has exceeded the heap memory max value of ~3.8 GB. Unsurprisingly then, the attempt to allocate 10 petabytes also slightly exceeds my current JVM limitations by just a bit.

The Airbrake-Java library provides real-time error monitoring and automatic exception reporting for all your Java-based projects. Tight integration with Airbrake’s state of the art web dashboard ensures that Airbrake-Java gives you round-the-clock status updates on your application’s health and error rates. Airbrake-Java easily integrates with all the latest Java frameworks and platforms like Spring, Maven, log4j, Struts, Kotlin, Grails, Groovy, and many more. Plus, Airbrake-Java allows you to easily customize exception parameters and gives you full, configurable filter capabilities so you only gather the errors that matter most.

Check out all the amazing features Airbrake-Java has to offer and see for yourself why so many of the world’s best engineering teams are using Airbrake to revolutionize their exception handling practices!