Advanced Debugging in Java

Naveen AnamaneniUpdated · 8 min read

A doctor doesn't examine every inch of a patient for every complaint. For a cough they listen to the chest; for a sprain they look at the ankle; and when they need to see something they can't reach, they use an X-ray. Debugging works the same way. Clicking "step over" through a whole program is fine for a ten-line exercise, but real bugs hide on the 4,000th loop iteration, inside an exception that is caught somewhere, or on a server you can't run on your laptop. In this lesson, you will learn the debugger features that take you straight to the problem: conditional breakpoints, exception breakpoints, evaluate expression, remote debugging and the command-line debugger jdb. The basics of breakpoints and stepping are in "Using the Debugger".

What Is Advanced Debugging?

Advanced debugging in Java means using the debugger's more precise tools to stop only where the bug is and inspect or test ideas without changing code. Every major IDE (IntelliJ IDEA, Eclipse, VS Code) offers the same core features, because they all talk to the JVM through the same protocol, JDWP (Java Debug Wire Protocol).

FeatureWhat it doesWhen to use it
Conditional breakpointStops only when a condition is true, such as i == 4000The bug appears only for certain data
Exception breakpointStops the moment an exception is thrown, even if it is caught laterAn exception is swallowed or its origin is unclear
Logpoint (non-suspending breakpoint)Prints a message instead of stoppingYou want a quick trace without editing code
Evaluate expressionRuns any Java expression on the paused programTesting a fix or a guess on the spot
Drop frameRewinds to the start of the current methodYou stepped past the interesting line
Remote debuggingDebugs a JVM running elsewhereBugs that only happen on a server or in a container

Conditional Breakpoints

A normal breakpoint inside a loop stops on every iteration. A conditional breakpoint stops only when its condition is true.

In IntelliJ IDEA, right-click the breakpoint dot (or press Ctrl+Shift+F8) and type a condition; in Eclipse, open Breakpoint Properties; in VS Code, right-click the gutter and choose Add Conditional Breakpoint. The condition is plain Java that can use any variable visible on that line:

Conditions.java
i == 4000                          // a specific iteration
student.name().equals("Advitha")   // a specific record
total < 0                          // a state that should never happen

Exception Breakpoints

When an exception is caught and logged far from where it was thrown, or caught and ignored, the stack trace you finally see may not lead to the cause. An exception breakpoint pauses the program at the exact line that throws, with every variable still in place.

  • IntelliJ IDEA: Run → View Breakpoints → + → Java Exception Breakpoints, then choose, for example, NullPointerException.
  • Eclipse: the J! button in the Breakpoints view.
  • VS Code: tick "Caught Exceptions" or "Uncaught Exceptions" in the Breakpoints panel.

Evaluate Expression

While the program is paused, Evaluate Expression (Alt+F8 in IntelliJ IDEA, Ctrl+Shift+I in Eclipse, the Debug Console in VS Code) runs any expression against the live program: call methods, inspect collections, or try a possible fix before editing the code. It is the fastest way to test a theory.

The Terminal Debugger jdb

The JDK includes jdb, a debugger that runs in the terminal. It is useful on servers without an IDE, and it shows clearly what every IDE does behind the scenes. Let's debug a program that should print the class average of 81.1 but prints 81.0.

Average.java▶ Try it
public class Average {
    static double average(int[] marks) {
        int total = 0;
        for (int i = 0; i < marks.length; i++) {
            total += marks[i];
        }
        return total / marks.length;
    }
 
    public static void main(String[] args) {
        int[] marks = {91, 84, 77, 95, 88, 60, 72, 69, 85, 90};
        System.out.println("average = " + average(marks));
    }
}
Output
average = 81.0

Compile with -g so the class file includes local variable names, then start jdb and stop on the return line:

Terminal
javac -g Average.java
jdb -sourcepath . Average
> stop at Average:7
> run
main[1] locals
main[1] print total
main[1] print marks.length
main[1] eval total / marks.length
main[1] cont
Terminal output
Initializing jdb ...
> Deferring breakpoint Average:7.
It will be set after the class is loaded.
> run Average
VM Started: Set deferred breakpoint Average:7
 
Breakpoint hit: "thread=main", Average.average(), line=7 bci=22
7            return total / marks.length;
 
main[1] Method arguments:
marks = instance of int[10] (id=432)
Local variables:
total = 811
main[1]  total = 811
main[1]  marks.length = 10
main[1]  total / marks.length = 81
main[1] > average = 81.0
 
The application exited

Explanation:

  1. stop at Average:7 sets a breakpoint on line 7. It is "deferred" until the class loads.
  2. locals lists the method's arguments and local variables: total is 811, which is correct.
  3. eval total / marks.length shows 81, not 81.1. Both operands are int, so Java does integer division and only then widens the result to double. The fix is (double) total / marks.length.
  4. cont lets the program finish.
jdb commandDescription
stop at Class:lineIt is used to set a breakpoint on a line.
stop in Class.methodIt sets a breakpoint at the start of a method.
catch java.lang.NullPointerExceptionIt stops when that exception is thrown (an exception breakpoint).
run, contThey start the program, or continue after a stop.
step, next, step upThey step into, step over, or run until the current method returns.
locals, print expr, dump objThey show local variables, an expression's value, or all fields of an object.
whereIt shows the call stack.
threads, thread idThey list threads, or switch to one.

Remote Debugging

Some bugs only happen on a test server or inside a container. Remote debugging starts that JVM with the JDWP agent, which listens on a port, and you attach your debugger to it, exactly as if the program were running locally.

Step 1: start the program with the agent.

Terminal
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=localhost:5005 Average
Terminal output
Listening for transport dt_socket at address: 5005

Step 2: attach from another terminal (or from the IDE).

Terminal
jdb -sourcepath . -attach localhost:5005
main[1] stop at Average:7
main[1] cont
main[1] print total
main[1] cont
Terminal output
Initializing jdb ...
VM Started: > No frames on the current call stack
main[1] Deferring breakpoint Average:7.
It will be set after the class is loaded.
main[1] > Set deferred breakpoint Average:7
 
Breakpoint hit: "thread=main", Average.average(), line=7 bci=22
7            return total / marks.length;
 
main[1]  total = 811
main[1] >
The application exited

Explanation:

  1. server=y makes the program listen for a debugger; suspend=y makes it wait for one before running main, so you don't miss the start. Use suspend=n for a server that should start normally.
  2. address=localhost:5005 accepts connections only from the same machine. In an IDE, create a "Remote JVM Debug" configuration with host and port 5005 instead of using jdb.
  3. After attaching, breakpoints and inspection work exactly as before.

In Practice

SymptomTool to reach for
Wrong result only for one record in a big listConditional breakpoint on that record
"Something threw an exception somewhere"Exception breakpoint on that type
Need a trace without stoppingLogpoint
Bug only on the test serverRemote debugging over an SSH tunnel
Program hangsA thread dump (jcmd <pid> Thread.print), then debug the stuck thread

Common Mistakes

Debugging without -g. Without debug information, jdb can't show local variable names. IDEs and build tools add it automatically.

Slow conditional breakpoints in hot loops. The condition is evaluated every time the line runs; in a loop of millions it can slow the program a lot. Narrow the location first.

Evaluate expression with side effects. Calling a method that changes state (such as list.remove(0)) really changes the paused program.

Leaving suspend=y on a server. The server won't start until a debugger attaches.

Practice

  1. Easy: Fix Average.java with a cast, run it again, and confirm it prints 81.1. Then use jdb with stop in Average.average and step to walk through the loop twice.
  2. Harder: Change the marks array to include a negative number by mistake. In your IDE, add a conditional breakpoint marks[i] < 0 inside the loop, and find which index holds the bad value without stepping through the others.

Quick check: Advanced debugging

4 questions
1What does a conditional breakpoint do?
2Which protocol do Java debuggers use to talk to the JVM?
3What does suspend=y mean in the JDWP agent options?
4Why compile with javac -g before using jdb?

Key Takeaways

Was this tutorial helpful?

Lesson 531 of 578