Making our way through our in-depth Java Exception Handling series, today we'll dig into the Java StackOverflowError. As with most programming languages, the StackOverflowError
in Java occurs when the application performs excessively deep recursion. However, what exactly qualifies as "excessively deep" depends on many factors.
In this article we'll explore the StackOverflowError
a bit more by first looking where it resides in the overall Java Exception Hierarchy. We'll also look at a simple, functional code sample that will illustrate how deep recursion can be created, and what might cause a StackOverflowError
in your own code. Let's get going!
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:
java.lang.Object
java.lang.Throwable
java.lang.Error
java.lang.VirtualMachineError
StackOverflowError
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.
package io.airbrake;
public class Main {
public static void main(String[] args) {
Iterator iterator = new Iterator();
iterator.increment();
}
}// Iterator.java
package io.airbrake;import io.airbrake.utility.Logging;
public class Iterator {
private static final double MILLION = 1000000.0;
private long count = 0;
// Create local reference to logging class, to avoid NoClassDefFoundErrors.
private Logging logger = new Logging();
private long startTime = System.nanoTime();Iterator() { }
void increment() {
try {
// Increment count and call self.
this.count++;
increment();
} catch (StackOverflowError error) {
// Get elapsed time.
long elapsed = System.nanoTime() - startTime;
// Output iteration count and total elapsed time.
logger.lineSeparator(String.format("%d iterations in %s milliseconds.", this.count, (double) elapsed / MILLION), 60);
// Output expected StackOverflowErrors.
logger.log(error);
} catch (Throwable throwable) {
// Output unexpected Throwables.
logger.log(throwable, false);
}
}
}// Logging.java
package io.airbrake.utility;import java.util.Arrays;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.builder.*;/**
* Houses all logging methods for various debug outputs.
*/
public class Logging {
private static final char separatorCharacterDefault = '-';
private static final String separatorInsertDefault = "";
private static final int separatorLengthDefault = 40;/**
* Get a String of passed char of passed length size.
* @param character Character to repeat.
* @param length Length of string.
* @return Created string.
*/
private static String getRepeatedCharString(char character, int length) {
// Create new character array of proper length.
char[] characters = new char[length];
// Fill each array element with character.
Arrays.fill(characters, character);
// Return generated string.
return new String(characters);
}/**
* Outputs any kind of Object.
* Uses ReflectionToStringBuilder from Apache commons-lang library.
*
* @param value Object to be output.
*/
public static void log(Object value)
{
if (value == null) return;
// If primitive or wrapper object, directly output.
if (ClassUtils.isPrimitiveOrWrapper(value.getClass()))
{
System.out.println(value);
}
else
{
// For complex objects, use reflection builder output.
System.out.println(new ReflectionToStringBuilder(value, ToStringStyle.MULTI_LINE_STYLE).toString());
}
}/**
* Outputs any kind of String.
*
* @param value String to be output.
*/
public static void log(String value)
{
if (value == null) return;
System.out.println(value);
}/**
* Outputs passed in Throwable exception or error instance.
* Can be overloaded if expected parameter should be specified.
*
* @param throwable Throwable instance to output.
*/
public static void log(Throwable throwable)
{
// Invoke call with default expected value.
log(throwable, true);
}/**
* Outputs passed in Throwable exception or error instance.
* Includes Throwable class type, message, stack trace, and expectation status.
*
* @param throwable Throwable instance to output.
* @param expected Determines if this Throwable was expected or not.
*/
public static void log(Throwable throwable, boolean expected)
{
System.out.println(String.format("[%s] %s", expected ? "EXPECTED" : "UNEXPECTED", throwable.toString()));
throwable.printStackTrace();
}/**
* See: lineSeparator(String, int, char)
*/
public static void lineSeparator() {
lineSeparator(separatorInsertDefault, separatorLengthDefault, separatorCharacterDefault);
}/**
* See: lineSeparator(String, int, char)
*/
public static void lineSeparator(String insert) {
lineSeparator(insert, separatorLengthDefault, separatorCharacterDefault);
}/**
* See: lineSeparator(String, int, char)
*/
public static void lineSeparator(int length) {
lineSeparator(separatorInsertDefault, length, separatorCharacterDefault);
}/**
* See: lineSeparator(String, int, char)
*/
public static void lineSeparator(int length, char separator) {
lineSeparator(separatorInsertDefault, length, separator);
}/**
* See: lineSeparator(String, int, char)
*/
public static void lineSeparator(char separator) {
lineSeparator(separatorInsertDefault, separatorLengthDefault, separator);
}/**
* See: lineSeparator(String, int, char)
*/
public static void lineSeparator(String insert, int length) {
lineSeparator(insert, length, separatorCharacterDefault);
}/**
* See: lineSeparator(String, int, char)
*/
public static void lineSeparator(String insert, char separator) {
lineSeparator(insert, separatorLengthDefault, separator);
}/**
* Outputs a dashed line separator with
* inserted text centered in the middle.
*
* @param insert Inserted text to be centered.
* @param length Length of line to be output.
* @param separator Separator character.
*/
public static void lineSeparator(String insert, int length, char separator)
{
// Default output to insert.
String output = insert;if (insert.length() == 0) {
output = getRepeatedCharString(separator, length);
} else if (insert.length() < length) {
// Update length based on insert length, less a space for margin.
length -= (insert.length() + 2);
// Halve the length and floor left side.
int left = (int) Math.floor(length / 2);
int right = left;
// If odd number, add dropped remainder to right side.
if (length % 2 != 0) right += 1;// Surround insert with separators.
output = String.format("%s %s %s", getRepeatedCharString(separator, left), insert, getRepeatedCharString(separator, right));
}
System.out.println(output);
}
}
When Should You Use It?
Before we look at what might cause a StackOverflowError
in Java code, let's first take a moment to review what a stack overflow
actually is. Most applications are allocated a range of memory addresses
that the application can use during execution. These addresses are stored and used as simple pointers to bytes of data (i.e. memory). This collection of addresses is known as the address space
assigned to the application, and it contains a specific range of memory addresses
that can be safely used by the application.
Unfortunately, at least for the foreseeable future, available memory is a finite resource. A Java application is limited to the bounds of its assigned address space
. Processes like garbage collection
will constantly free up memory that is no longer in use, but, by and large, there is a limited quantity of memory addresses
available to any given application.
When an application attempts to use memory outside of its assigned address space
a stack overflow
error typically occurs. The runtime that is handling the application cannot safely allow said application to use memory that hasn't been assigned to it, so the only logical course of action is to throw an error of some sort. In the case of Java, this is where the StackOverflowError
comes in.
There are many different ways a stack overflow
can occur within any given application, but one of the most common (and easily understood) is infinite recursion
. This essentially means that a function or method is calling itself, over and over, ad nauseam. Different languages handle infinite recursion differently, but the Java Virtual Machine (JVM
) handles infinite recursion by eventually throwing a StackOverflowError
. To illustrate this behavior our example code is quite simple, primarily performed in the Iterator
class:
// Iterator.java
package io.airbrake;import io.airbrake.utility.Logging;
public class Iterator {
private static final double MILLION = 1000000.0;
private long count = 0;
// Create local reference to logging class, to avoid NoClassDefFoundErrors.
private Logging logger = new Logging();
private long startTime = System.nanoTime();Iterator() { }
void increment() {
try {
// Increment count and call self.
this.count++;
increment();
} catch (StackOverflowError error) {
// Get elapsed time.
long elapsed = System.nanoTime() - startTime;
// Output iteration count and total elapsed time.
logger.lineSeparator(String.format("%d iterations in %s milliseconds.", this.count, (double) elapsed / MILLION), 60);
// Output expected StackOverflowErrors.
logger.log(error);
} catch (Throwable throwable) {
// Output unexpected Throwables.
logger.log(throwable, false);
}
}
}
As you can see, we have a few private members, along with the increment()
method, which attempts a simple task: Iterate the count
field, then call itself again. We also catch the potential errors (or Throwables
) that might come up from this process.
Let's create a new instance of Iterator
and start the recursive process by calling the increment()
method:
package io.airbrake;
public class Main {
public static void main(String[] args) {
Iterator iterator = new Iterator();
iterator.increment();
}
}
Executing the few lines of code above produces the following output:
-------- 7189 iterations in 5.202404 milliseconds. ---------
[EXPECTED] java.lang.StackOverflowError
We can see that a StackOverflowError
was thrown, as expected, and it took about 7,200
iterations before the error occurred, with a total processing time of about 5.2
milliseconds. This is just one test, so let's run it a few more times and record the results:
-------- 7294 iterations in 5.846793 milliseconds. ---------
-------- 7545 iterations in 4.776957 milliseconds. ---------
-------- 8050 iterations in 5.783158 milliseconds. ---------
-------- 7307 iterations in 4.498283 milliseconds. ---------
-------- 7305 iterations in 5.445483 milliseconds. ---------
What's immediately interesting is that, while a StackOverflowError
is thrown every time, the number of recursive iterations necessary to cause the error changes every time, but within a reasonably similar range. The reason for this difference is due to the vast quantity of different factors within the system when execution occurs. For example, the JVM I'm testing this on is Windows 10 64-bit with 16GB of memory, but if we run this application on other machines (with different JVM configurations), we might see completely different iteration counts and/or elapsed times.
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! Try Airbrake free for 30 days.