Tutorial: Java Debugging Deep Dive
Debugging is one of the most powerful tools in any developer's arsenal. It gives us a unique insight into how a program runs and allows us to gain a much deeper understanding of the piece of code we debug. It allows us to trace running code and inspect the state and the flow of the execution. As part of that, it gives us the illusion of a sequential flow. This is very intuitive and powerful but also may be misleading as most modern applications are multithreaded.
"Debugging" suggests we deal with bugs but this is actually a misnomer. The information we get from debugging is useful even when there is no problem with the code. Finding bugs just happens to be a very common use case for the knowledge we can get from a debug session.
The IntelliJ IDEA debugger offers a rich experience that helps us to easily debug anything from the simplest code to complex multithreaded applications.
Before we start, a word of caution: debugging is a very powerful tool but it does come with a cost. The debug process is part of the runtime and therefore affects it. Every evaluation of an expression happens using the same memory of the debugged application, and can modify and potentially corrupt the state. During this tutorial, bear in mind that debugging is an intrusive approach that may affect the outcome of the debugged application. We will explore a few ways to minimize its impact and sometimes even exploit it. The timing of execution is also very different when you debug code compared to running it. The minimal debug tracking overhead in itself may already be enough to change the timing of events and therefore the application behaviour. Every breakpoint or log is a possible synchronization point, and stepping obviously changes the timings significantly. As we are about to see, this becomes a critical issue in multithreaded environments, when reproducing a bug sometimes depends on a very specific sequence of events, which a change in timing can make unreachable.
Last point to remember is that debugging is not a substitute for understanding the code. In fact, the only way to learn from a debug session is to constantly compare the information the debugger shows us with our expectations from the code and how we think it "should" behave. Before starting a debugging session we must have some knowledge of what we're trying to achieve by it. If we're looking for a bug, we need to roughly know what is incorrect, i.e. what is different from the expected behaviour or state. In most cases we will also have some initial assumption as to why things are wrong. This will dictate how our debugging session should be conducted. When we debug, we must always compare that information with our expectations, and pay close attention when the code deviates from that, as that is the point where it is so effective. That is the point where we learn.
In this tutorial: