Debugging Java Programs in Your IDE and with jdb

Introduction Every programmer writes bugs. The difference between a frustrating afternoon and a five-minute fix is usually how you look for them. A debugger lets you pause a running program at any line, look at the value of every variable, and step through the code one statement at a time - watching exactly where reality differs from what you expected. This tutorial explains debugging Java programs, in your IDE and with the JDK's command-line debugger, jdb. We will start with a buggy program and the core ideas of debugging - breakpoints, stepping and inspecting. We will then go through the debugger controls and shortcuts in IntelliJ IDEA, Eclipse and VS Code. Next we will find the bug in a real jdb session. Lastly, we will look at conditional breakpoints, debugging exceptions, remote debugging, and a step-by-step approach to finding bugs.

(Note - Setting up an IDE is covered in Choosing a Java IDEClick to read tutorial: Choosing a Java IDE: IntelliJ IDEA vs Eclipse vs VS Code.) A program with a bug This program should print the average of three marks - 70, 85 and 90 - which is 81.67. It compiles, runs, and prints the wrong answer.
A program with a bug
//AverageBug.java
public class AverageBug {
  static double average(int[] values) {
    int sum = 0;
    for (int v : values) {
      sum += v;
    }
    double avg = sum / values.length;
    return avg;
  }

  public static void main(String[] args) {
    int[] marks = {70, 85, 90};
    System.out.println("Average: " + average(marks));
  }
}
 OUTPUT of the above code
Average: 81.0
The method returns a double, and avg is a double, so where did the fraction go? Rather than guessing, let us pause the program and look. Core debugging concepts
  • Breakpoint: a marker on a line. When execution reaches it, the program pauses before running that line.
  • Inspecting variables: while paused, you can see the parameters, local variables and fields of the current method, and evaluate any expression.
  • Step over: run the current line, including any method it calls, and pause on the next line.
  • Step into: if the current line calls a method, go inside it and pause on its first line.
  • Step out: finish the current method, and pause in the caller right after the call.
  • Resume: carry on running until the next breakpoint, or the end of the program.
  • Call stack: the chain of method calls which led to the current line - the same frames described in Stack vs Heap MemoryClick to read tutorial: Stack vs Heap Memory in Java and Object References. Clicking a frame shows that method's variables.
The debugger can only show local variable names if the classes were compiled with debugging information. IDEs and build tools do this by default; with javac, add the -g option.
Debugging in the IDE In every major IDE, you set a breakpoint by clicking in the left margin (the gutter) next to a line number - a red dot appears - and then start the program in Debug mode instead of Run mode, with the bug icon next to the run button. When the breakpoint is hit, the IDE highlights the line and shows the Variables and Call Stack panels. The stepping shortcuts are -
ActionIntelliJ IDEAEclipseVS Code
Start debuggingShift+F9F11F5
Toggle breakpointCtrl+F8Ctrl+Shift+BF9
Step overF8F6F10
Step intoF7F5F11
Step outShift+F8F7Shift+F11
ResumeF9F8F5
Evaluate expressionAlt+F8Ctrl+Shift+I (Inspect)Debug Console

These are the default Windows and Linux keymaps; on macOS some shortcuts use Cmd or function keys with Fn. For our bug, put a breakpoint on the line double avg = sum / values.length;, debug the program, and look at sum; then step over and look at avg. The command-line session below shows exactly what you would see. Finding the bug with jdb jdb, the debugger in the JDK's bin folder, offers the same features from the command line - useful on servers without an IDE, and a good way to see what an IDE's debugger does under the hood. Start it with the class to debug (compiled with javac -g), then type commands at its prompt -
  • stop at Class:line - set a breakpoint; stop in Class.method - break when a method is entered.
  • run - start the program; cont - resume after a breakpoint.
  • locals - show the current method's parameters and local variables; print expression - evaluate an expression.
  • next - step over; step - step into; step up - step out; where - show the call stack.
javac -g AverageBug.java
jdb AverageBug
 OUTPUT - a jdb session (the commands typed are after each prompt)
Initializing jdb ...
> stop at AverageBug:8
Deferring breakpoint AverageBug:8.
It will be set after the class is loaded.
> run
run AverageBug
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
>
VM Started: Set deferred breakpoint AverageBug:8

Breakpoint hit: "thread=main", AverageBug.average(), line=8 bci=33

main[1] locals
Method arguments:
values = instance of int[3] (id=466)
Local variables:
sum = 245
main[1] print sum / values.length
 sum / values.length = 81
main[1] next
>
Step completed: "thread=main", AverageBug.average(), line=9 bci=39

main[1] print avg
 avg = 81.0
main[1] print sum * 1.0 / values.length
 sum * 1.0 / values.length = 81.666664
main[1] cont
> Input stream closed.
Average: 81.0
Explanation of the session
  • stop at AverageBug:8 set a breakpoint on line 8, the division (line 1 is the //AverageBug.java comment). The class was not loaded yet, so jdb deferred it until run loaded the class.
  • At the breakpoint, locals showed sum = 245 - correct. So the loop is fine, and the bug must be on line 8 itself.
  • print sum / values.length gave 81: an int divided by an int is an int, so the fraction is dropped before the result is stored in the double. After next, avg was 81.0.
  • Evaluating sum * 1.0 / values.length confirmed the fix - making one operand a double. (jdb's simple expression evaluator shows the result with less precision, 81.666664; the program itself would compute 81.66666666666667.) The bug is integer division, explained in Type Conversion and CastingClick to read tutorial: Type Conversion and Casting in Java with Examples.
The fix is one cast: double avg = (double) sum / values.length;, which gives 81.66666666666667.
More powerful debugging features
  • Conditional breakpoints: right-click a breakpoint and add a condition, such as i == 999 or name.equals("Ethan"). The program pauses only when the condition is true - essential for bugs that happen on the 1000th loop iteration.
  • Exception breakpoints: pause the moment a given exception is thrown - for example every NullPointerException - showing the exact state that caused it, before the stack unwinds. In jdb, use catch java.lang.NullPointerException.
  • Watches: expressions the IDE re-evaluates at every pause, such as orders.size().
  • Changing values: most debuggers let you set a variable to a new value while paused, to test a theory without restarting.
  • Logpoints: breakpoints which print a message instead of pausing - like adding a println() without changing the code.
  • Remote debugging: start a program with -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005, and the IDE (or jdb -attach 5005) can connect to it - even on another machine. Never leave this enabled on a production server open to the network.
A systematic approach to finding bugs
  1. Reproduce the bug reliably, with the smallest input that shows it.
  2. Read the evidence - the exact error message and the stack traceClick to read tutorial: Checked vs Unchecked Exceptions in Java; the first line of your own code in it is usually the place to start.
  3. Form a hypothesis about where the actual and expected behaviour diverge.
  4. Test it with the debugger - break before the suspect code, check the values going in, step over it, and check the values coming out. Narrow the area down, as we did by checking sum before looking at the division.
  5. Fix the cause, not the symptom, then write a test which would have caught the bug - see unit testing with JUnit 5Click to read tutorial: Unit Testing Basics with JUnit 5 in Java.
System.out.println() debugging still has its place for quick checks, but it means editing, recompiling and removing the prints afterwards - while the debugger shows everything, at any point, without changing the code. Summary In this tutorial we first met a program which printed a wrong average, and the core ideas of debugging - breakpoints, inspecting variables, stepping over, into and out, and the call stack. We then went through the debugger controls and shortcuts in IntelliJ IDEA, Eclipse and VS Code. Next we found the bug in a real jdb session, by checking sum, evaluating the division, and confirming the fix - integer division. Lastly, we looked at conditional and exception breakpoints, watches, logpoints and remote debugging, and a systematic approach to finding bugs. In the next tutorial, we will build projects with Maven and GradleClick to read tutorial: Introduction to Maven and Gradle for Java Projects.