avatarUğur Taş

Summary

The provided web content discusses the mechanisms of exception handling in Java, detailing the creation of exception objects, stack trace generation, execution flow unraveling, call stack unwinding, exception propagation, and the distinction between caught and uncaught exceptions.

Abstract

Java's exception handling is a robust framework designed to manage and recover from errors that disrupt normal program execution. When an exception occurs, an exception object is instantiated on the heap, encapsulating information about the error. A stack trace is generated, providing a snapshot of the call stack, which aids in debugging. The execution flow is halted in the current method, and the call stack begins to unwind as the runtime system searches for an appropriate exception handler. If a suitable catch block is found, the exception is handled, allowing the program to continue; otherwise, the thread may terminate, and if it's a non-daemon thread, the entire application may shut down. The article emphasizes the importance of proper exception handling to prevent abrupt program crashes and ensure code stability.

Opinions

  • The author suggests that exception handling is a critical aspect of Java programming, indicating its significance in writing stable and robust applications.
  • The article implies that understanding the behind-the-scenes process of exception handling, including stack trace generation and call stack unwinding, is crucial for effective debugging and error management.
  • By distinguishing between caught and uncaught exceptions, the author conveys the necessity of implementing appropriate exception handlers to gracefully manage errors and potentially recover from them.
  • The use of thread-specific uncaught exception handlers is presented as a method to customize the error handling process, allowing for more sophisticated recovery strategies.
  • The article advocates for the proper use of exceptions as a key practice in Java development, highlighting the language's capabilities to handle errors elegantly and maintain application integrity.

Exception, What Happens Behind the Scenes in Java

Exceptions are events that disrupt the normal flow of program execution in Java. When an exception occurs, the default behavior is for the program to halt abruptly and print out an error message. However, Java provides robust exception handling mechanisms that allow developers to anticipate and recover gracefully from exceptions.

What is an Exception?

An exception in Java is an event that disrupts the normal flow of a program. It’s an object that represents an error or an unusual situation. Java categorizes exceptions into two main types: checked exceptions and unchecked exceptions.

There are various circumstances that trigger exceptions in Java. Firstly, They can be due to errors in code, such as trying to access an array element out of its bounds. The second possible reason is external factors, such as a file does not exist.

When such situations occur, Java creates an object that encapsulates the error information. This object is the exception.

Example: Consider a scenario where you attempt to access the fifth element of a four-element array:

int[] numbers = {1, 2, 3, 4}; 
int value = numbers[4]; // This line throws an ArrayIndexOutOfBoundsException

In this case, Java identifies the error and creates an ArrayIndexOutOfBoundsException object.

Behind the scenes, several key steps take place when an exception occurs in Java. We will go over them below.

Exception Object Creation

When an exception occurs, an exception object is created to encapsulate information about the error. That exception object is instantiated in the heap memory.

This is because exceptions in Java are objects derived from the Throwable class. In addition to that, exception objects often need to persist beyond the scope of the current method. However, the stack memory is typically small and has a limited lifespan.

When a piece of code encounters an error or an unexpected situation, it throws an exception. This can happen explicitly through the throw statement or implicitly when a predefined condition is met. For example, consider the following code snippet:

public class ExceptionExample {
    public static void main(String[] args) {
        calculate(10, 0);
        System.out.println("Result: ");
    }

    public static void calculate(int num1, int num2) {
        int result = divide(num1, num2);
        sum(num1, num2, result);
    }

    public static int sum(int num1, int num2, int num3) {
        return num1 + num2 + num3;
    }

    public static int divide(int numerator, int denominator) {
        return numerator / denominator;
    }
}

In this example, the divide method attempts to divide a number by zero, triggering an ArithmeticException at runtime.

Stack Trace Creation

The stack trace in Java is typically generated at the point where an exception is created or thrown. It represents the state of the call stack at that particular moment. The creation of the stack trace includes information about the sequence of method calls leading up to the point where the exception occurred.

The stack trace does not continually grow during the unwinding process. Instead, it remains constant from the point of the exception’s creation. The stack trace is a snapshot of the call stack’s state at the time the exception creation.

You can see the sample stack trace of the above code:

Exception in thread "main" java.lang.ArithmeticException: / by zero
 at org.mugurtas.medium.exception.ExceptionExample.divide(ExceptionExample.java:19)
 at org.mugurtas.medium.exception.ExceptionExample.calculate(ExceptionExample.java:10)
 at org.mugurtas.medium.exception.ExceptionExample.main(ExceptionExample.java:5)

Execution Stopped in Current Method

After the exception object is created, the execution of the current method is stopped. Control jumps immediately to the exception handling section, bypassing any remaining code in the method. This ensures that the method cannot continue execution in an invalid state after the exception occurs.

In the above example, divide will throw an exception and the remaining part of the method will not run.

Unraveling the Execution Flow

Java relies on the call stack to manage the sequence of method calls, when a code throws an exception. Because the call stack keeps all method calls leading to the point of the exception.

The call stack is a data structure that keeps track of method calls and their respective contexts. Each method call pushes a new frame onto the stack.

Thanks to the call stack, exceptions can find the execution flow and propagate through it step by step.

Call Stack Unwinding Begins

When an exception occurs, the runtime looks for a suitable exception handler to address the issue. If it cannot find a proper handler, it unwinds the call stack. This means that it removes the method entries from the call stack. These entries include the local variables, object references, etc.

This is a crucial step in memory management during exception handling. Because this might involve invoking destructors for objects or releasing acquired resources.

Let’s visualize this process with a step-by-step breakdown:

  1. The divide method is called with arguments 10 and 0.
  2. Within the divide method, an attempt to divide 10 by 0 triggers an ArithmeticException.
  3. This causes an exception creation and stack trace generation.
  4. Execution of the current method stops.
  5. Starts looking for a proper exception handler in the current method.
  6. Call stack unwinding begins

Propagation of Exception

Once an exception object is created, it doesn’t stay put. It begins a journey up the stack of method calls (the call stack). This journey is known as “propagation”.

Once the current method exits, the exception will propagate up the call stack to the methods that invoked the current method. The exception moves from the method where it occurred up through the method call hierarchy. This unwinding of the call stack continues until the exception is handled or until the program terminates if no suitable handler is present.

Let’s visualize this process with a step-by-step breakdown:

  1. After unwinding begins, The exception propagates up the call stack, searching for an appropriate exception handler in the hierarchy.
  2. In this scenario, Java looks for a suitable catch block, either in divide or its calling methods (mainand calculate).

Caught vs. Uncaught Exceptions

When an exception is thrown, Java looks for a suitable catch block to handle the exceptional condition. If a matching catch block is found, the exception is considered caught, and the program can continue its execution.

However, if no appropriate handler is present, the exception becomes uncaught, leading to the termination of the program with an error message and a stack trace.

A. Catching the Exception

If the propagating exception matches the type declared in a catch block in the call stack, that exception handler’s code will be run. This allows recovery from the exception. For example, the catch block may log the error, close resources, or display a user-friendly message.

Here is a detailed explanation of what happens:

  1. The catch block matching the exception type gets triggered. Execution jumps here from the point where the exception was thrown.
  2. The caught exception is passed as an argument to the catch block. This allows access to the exception details like message, stack trace etc.
  3. The statements inside the catch block can then attempt to recover from the specific exception. For example, retry an operation, log the problem, or display a user-friendly message.
  4. After the catch block finishes execution, program flow continues with the statements immediately after the try/catch block. So normal code execution resumes.
  5. If the exception was successfully handled in the catch block, then the remaining program can keep running as if nothing went wrong originally.
  6. So catching and handling exceptions provides a graceful recovery mechanism compared to abrupt crashes when exceptions go uncaught.

B. The Uncaught Exceptions

What happens if an exception is not caught?

1- Reach the End of the Call Stack

If the exception remains unhandled even after unwinding the entire call stack, it reaches the Java Runtime Environment (JRE). And then the thread is terminated.

At this point, thread specific uncaught exception handler starts working. If there is no specific handler, then the default exception handler kicks in. Default handler prints the exception’s stack trace to the console. Stack trace shows where the exception occurred and how it propagated.

2- Thread-Specific Uncaught Exception Handler

If the thread has a thread-specific uncaught exception handler that handler starts working to handle the uncaught exception. The thread class has a method to set a specific handler. The method is Thread.setUncaughtExceptionHandler() .

class ExceptionHandler implements Thread.UncaughtExceptionHandler {

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        // Prints exception message
        System.out.println("Exception message: " + e.getMessage());

        // Logs full exception details
        e.printStackTrace();

        // Custom handling then allows thread to stay alive
        System.out.println("Performing custom recovery before continuing...");
    }

}

public class ThreadSpecificHandler {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                expThrow();
                System.out.println("After exception");
            }

            public void expThrow() {
                throw new RuntimeException("Something went wrong!");
            }
        });

        // Register custom handler
        thread.setUncaughtExceptionHandler(new ExceptionHandler());
        thread.start();

        thread.join();

        System.out.println("Non-Daemon thread does not have exception. So it continue to work");
    }
}

3- Thread Termination

Let’s look at the overview of what happens after a thread encounters an uncaught exception in Java:

  • The thread immediately stops executing any further code. Its run() method exits abruptly.
  • The thread becomes dead or non-running. Restarting it is not possible.
  • Any locked monitors or synchronized blocks held by the thread are released. This allows other threads waiting on those locks to proceed.
  • Any daemon threads spawned by the terminated thread also complete execution.
  • Resources like open files or network connections that were being used by the thread may be closed or become eligible for garbage collection.
  • If the terminated thread was a non-daemon user thread, it will cause the entire Java program to quit.
  • If the terminated thread was a daemon thread, the JVM will continue running normally with other non-daemon threads. You can see it from the above example.
  • If the thread was holding CPU resources and time slice, those are returned to the system and made available to other threads.
  • The stack frame and execution context of the thread are discarded and become available for garbage collection.

So in summary, thread termination cleanly releases resources, stops further execution, and allows the program or JVM to recover from the uncaught exception. All execution in the thread halts permanently.

4- Application Termination

Here is the how application terminates after an uncaught exception.

  • If the exception occurs in the main thread and causes it to abort, the application will immediately quit. The main thread is non-daemon by default and critical for the app to run.
  • If the exception occurs in any non-daemon user thread, the abrupt termination of that thread will cause the entire application to quit.
  • The JVM allows non-daemon threads to terminate naturally before shutting down. An uncaught exception forcibly terminates the thread before completion.
  • When the JVM detects a non-daemon thread has terminated unexpectedly, it will initiate shutdown of the entire application.
  • All other threads including daemon threads will then complete execution before the app termination.
  • Resources used by the application such as open files or network connections will be closed.
  • Any uncaught exception handlers at the app level will be invoked before exiting.

In summary, exceptions allow Java programs to handle errors gracefully by providing exception propagation, stack unwinding, and exception handlers. This prevents abrupt crashes, makes code more robust, and allows recovery from unexpected errors. Proper use of exceptions is key to writing stable Java applications.

👏 Thank You for Reading!

👨‍💼 I appreciate your time and hope you found this story insightful. If you enjoyed it, don’t forget to show your appreciation by clapping 👏 for the hard work!

📰 Keep the Knowledge Flowing by Sharing the Article!

✍ Feel free to share your feedback or opinions about the story. Your input helps me improve and create more valuable content for you.

✌ Stay Connected! 🚀 For more engaging articles, make sure to follow me on social media:

🔍 Explore More! 📖 Dive into a treasure trove of knowledge at Codimis. There’s always more to learn, and we’re here to help you on your journey of discovery.

Java
Exception
Exception Handling
Exceptions In Java
Recommended from ReadMedium