Java Performance: Using Exceptions as Intended

By Virendra Mehta

Date: Sep 17, 2004

Return to the article


Virendra Mehta explains that, while exceptions are a useful facility in Java, allowing us to handle errors and other unusual circumstances in an appropriate manner, they also have limitations that we must take into consideration.

Exceptions in Java

Exceptions are an important part of most object-oriented languages, including the two most common languages today—C++ and Java. They provide a good way of handling errors and other exceptional situations, allowing the program to recover from an unusual incident and continue, or to quit gracefully. Java makes it especially easy by allowing an exception to be represented by an object—possibly sub-classed from the Exception class—and thus exceptions are used very widely in Java programming.

Listing 1 shows different ways of using exceptions:

Listing 1

import java.lang.*;
public class DivNumbers {
   public static void testMe(int x, int y) throws Exception {
      if (y == 0)
         throw new Exception("Dividing by zero");
   }
   public static void main(String args[]) {
      try {
         int x = Integer.parseInt(args[0]);
         int y = Integer.parseInt(args[1]);

         testMe(x, y);
         System.out.println("The result is " + x/y);
      } catch (ArrayIndexOutOfBoundsException arrEx) {
         System.out.println(
           "Usage: java DivNumbers numerator denominator");
      } catch (Exception e) {
         System.out.println(e);
      } finally {
         System.out.println("Thank you for using me");
      }
   }
}

This example shows the implementation of a class that returns the result of division of two numbers passed on the command line. The main method invokes a private method that checks whether the denominator is zero. If so, it throws an instance of the Exception object. If the denominator is nonzero, testMe should return quietly and main should print the output of the division. Otherwise, Exception is thrown and caught by the catch clause, which uses the general case of Exception, and an error message is printed. Another interesting case is that while reading the numbers to be divided, if the user doesn't pass at least two numbers, we could run into an out-of-bounds exception while accessing args. This prompts a Usage message. Finally, the finally clause ensures that whatever we do, we would execute the "thank you" message.

Using Exceptions To Write Elegant Code

Exceptions provide a way to write elegant code by keeping rare situations from cluttering up the rest of the code, thereby improving readability.

We would never use the exception from Listing 1 by checking for the exceptions individually, as in Listing 2:

Listing 2

int x = 1, y = 1; // avoids warnings about uninitialized variables

try {
   x = Integer.parse(args[0]);
} catch (ArrayIndexOutOfBoundsException ae1) {
   System.out.println(usageString);
}

try {
   y = Integer.parse(args[1]);
} catch (ArrayIndexOutOfBoundsException ae2) {
   System.out.println(usageString);
}

try {
   testMe(x, y);
   System.out.println("The result is " + x/y);
} catch (Exception ex) {
} finally { System.out.println(thanksString); System.exit();
}

// ...

It's much better and easier to collect the exceptions at the end of the code. In this example, we could have done a quick if check to see whether the number of arguments match the expected value. But in more involved cases we may need several such checks in different places, and they tend to clutter up the code. Also, in some cases, the issues may be within called methods, and we may have to transfer the error situation up a chain.

In some cases, it may be much more readable and effective to collect all exceptions at the end of the block. This works even better if the thrown exception object is an instance of a subclass used for all exceptions. We could hold the type of exception as one of the attributes of the object, and use that in the handler to take appropriate action. Such techniques result in more readable code, as shown in Listing 3.

Listing 3

try {
   Object o = readInput();
   AnalyzeHeader(o);
   OutputHeader(o);
   AnalyzeBody(o);
   OutputBody(o);
   AnalyzeFooter(o);
   OutputFooter(o);
} catch (MyException mEx) {
   switch(mEx.getCode()) {
      case 1: System.out.println("Bad input"); break;
      default: System.out.println(mEx);
   }
}

The Problem: Using Exceptions for Control-Flow

In some programming situations, it's easy to use exceptions as a way of controlling the normal execution. In such cases, an exception is not used as an unexpected error condition, but rather in place of a conditional or counted termination of a regular loop, or to handle the rare (but not erroneous) case of a conditional statement. In such cases, the exception is handled and execution continues in a planned manner. Most of the time, these exceptions can be avoided by rewriting code to have explicit checks—though sometimes at the cost of some readability.

One situation that lends itself more easily to this type of coding is when converting a finite-state machine algorithm to a program. Usually there's a lot of string processing that uses buffers of sizes that may not match up. Instead of introducing a check at every point, it would be easier to simply code everything—using exceptions to refill buffers and using global variables as indices. The result would be similar to the method shown in Listing 4.

Listing 4

static int countCharsWithExcep() {
   char[] locBuffer = new char[5];
   int locIdx = 0;
   int numExp = 0;
   while (globCounter++ < maxIter) {
      while(true) {
         try {
           locBuffer[locIdx] = globalBuffer[globIdx++];
           updateCounter(locBuffer[locIdx++]);
         } catch (ArrayIndexOutOfBoundsException ex) {
           ++ numExp;
           if (locIdx >= locBuffer.length) {
              locIdx = 0; --globIdx; continue;
           }
           else break;
         }
      }
      globIdx = 0;
   }
   return numExp;
}

Instead of explicitly checking for array indices being out of bounds, this is handled in the exception-handling code, and execution continues after resetting the indices. While the example is a small part of a larger program that uses this philosophy in a more complex way, it still serves to prove the point about Java performance.

We might rely on good exception-handling performance from the underlying Java virtual machine (JVM) and avoid using conditionals, believing that would impair the performance of the common case. But most JVMs handle exceptions as rare and unexpected events. The JVM has to potentially unwind the call stack all the way to the top calling method to hunt for a handler for the exception. Even if a handler is available in the current method itself, some JVMs may still jump from native generated code to handle the exception code in the runtime system. Such JVM mechanisms to handle exceptions can provide acceptable performance so long as exceptions are rare; the simplicity of this implementation can take precedence over improving the performance of a rare condition (which, moreover, could end in termination of the program). If the programmer uses an exception more commonly, a particular method can become a major bottleneck.

We can modify the countChars code in Listing 4 to use conditionals instead of exceptions, as shown in Listing 5.

Listing 5

static void countCharsNoExcep() {
   char[] locBuffer = new char[5];
   int locIdx = 0;
   while (globCounter++ < maxIter) {
        while(true) {
         if (locIdx >= locBuffer.length) {
            locIdx = 0;
         }
         if (globIdx >= globalBuffer.length)
            break;
         locBuffer[locIdx] = globalBuffer[globIdx++];
         updateCounter(locBuffer[locIdx++]);
        }
        globIdx = 0;
   }
}

Performance Tests of the Two Code Samples

To understand the effects of using exceptions instead of conditionals, we ran a test using the two examples, countCharsWithExcep in Listing 4 and countCharsNoExcep in Listing 5. The two methods were called the same number of times. To ensure that the methods got compiled, we called them 100 times; 1,000 times; 5,000 times, and 10,000 times. The test was done on a laptop running Windows XP using several versions of commercial JVMs from three vendors. The results are presented in the following table. The time taken for countCharsNoExcep was less than 1 millisecond (ms) for all the runs. But for countCharsWithExcep, the times ranged from 10 ms at 1,000 iterations to 260 ms at 10,000 iterations. The actual time varied for each JVM; we believe that with sufficient tuning applied, some of these numbers could be reduced and maybe even all fall to the same level. But it was very clear that the case with exceptions showed at least an order of magnitude extra cost above the one without exceptions.

JVM

100 Iterations

1,000 Iterations

5,000 Iterations

10,000 Iterations

No Ex

340 Ex

No Ex

3,400 Ex

No Ex

17,000 Ex

No Ex

34,000 Ex

 

JVM 1

<1 ms

10 ms

<1 ms

30 ms

<1 ms

140 ms

<1 ms

261 ms

JVM 2

<1 ms

<1 ms

<1 ms

80 ms

<1 ms

170 ms

<1 ms

260 ms

JVM 3

<1 ms

10 ms

<1 ms

70 ms

<1 ms

190 ms

<1 ms

311 ms

JVM 4

<1 ms

<1 ms

<1 ms

10 ms

<1 ms

70 ms

<1 ms

140 ms

JVM 5

<1 ms

<1 ms

<1 ms

10 ms

<1 ms

30 ms

<1 ms

40 ms


This is a micro-benchmark, and therefore it's not easy to extrapolate the results to a real application. But the actual results could vary drastically based on the application profile. For example, this method could be in a critical region where a slowdown could ripple through a lot of other components that are waiting for a module to finish. Because Java is used extensively in distributed computing, multiple systems could be affected, as we found in some commercial systems. It could also lead to different timing windows on different JVMs, and cause unexpected behavior when you move the application from one system to another.

Summary

While exceptions are a useful facility in Java and allow us to handle errors and other unusual circumstances in an appropriate manner, they carry a performance penalty. This penalty can cause major bottlenecks in a program if exceptions are not used the way they were intended to be used, but instead as a way to control the flow of execution. While this may be acceptable in some situations where readability of code is most important, if the code is in a critical region, this way of using exceptions should definitely be avoided.