Debugging with JDB
The Java Debugger (JDB) is a command-line debugger that ships with the JDK. JDB is at least conceptually a descendant of the Gnu Debugger (GDB, which was inspired by the original Unix DB), in terms of the instructions for debugging and its command-line interface. JDB is useful for learning about debugging and performing simpler debugging tasks, and it’s helpful to know that it’s always available wherever the JDK is installed. However, for larger projects you’ll probably want to use a graphical debugger, described later.
Suppose you’ve written the following program:
//: c15:SimpleDebugging.java
// {ThrowsException}
public class SimpleDebugging {
private static void foo1() {
System.out.println("In foo1");
foo2();
}
private static void foo2() {
System.out.println("In foo2");
foo3();
}
private static void foo3() {
System.out.println("In foo3");
int j = 1;
j--;
int i = 5 / j;
}
public static void main(String[] args) {
foo1();
}
} ///:~
If you look at foo3( ), the problem is obvious; you’re dividing by zero. But suppose this code is buried in a large program (as is implied here by the sequence of calls) and you don’t know where to start looking for the problem. As it turns out, the exception that will be thrown will give enough information for you to locate the problem (this is just one of the great things about exceptions). But let’s just suppose that the problem is more difficult than that, and that you need to drill into it more deeply and get more information than what an exception provides.
To run JDB, you must tell the compiler to generate debugging information by compiling SimpleDebugging.java with the –g flag. Then you start debugging the program with the command line:
jdb SimpleDebugging
This brings up JDB and gives you a command prompt. You can view the list of available JDB commands by typing ‘?’ at the prompt.
Here’s an interactive debugging trace that shows how to chase down a problem:
Initializing jdb ...
> catch Exception
The > indicates that JDB is waiting for a command, and the commands typed in by the user are shown in bold. The command catch Exception causes a breakpoint to be set at any point where an exception is thrown (however, the debugger will stop anyway, even if you don’t explicitly give this comment—exceptions appear to be default breakpoints in JDB).
Deferring exception catch Exception.
It will be set after the class is loaded.
> run
Now the program will run until the next breakpoint, which in this case is where the exception occurs. Here’s the result of the run command:
run SimpleDebugging
>
VM Started: In foo1
In foo2
In foo3
Exception occurred: java.lang.ArithmeticException (uncaught)"thread=main", SimpleDebugging.foo3(), line=18 bci=15
18 int i = 5 / j;
The program runs until line 18, where the exception generated, but JDB does not exit when it hits the exception. The debugger also displays the line of code that caused the exception. You can list the point where the execution stopped in the program source by the list command as shown here:
main[1] list
14 private static void foo3() {
15 System.out.println("In foo3");
16 int j = 1;
17 j--;
18 => int i = 5 / j;
19 }
20
21 public static void main(String[] args) {
22 foo1();
23 }
The pointer (“=>”) in this listing shows the current point from where the execution will resume. You could resume the execution by the cont (continue) command. But doing that will make JDB exit at the exception, printing the stack trace.
The locals command dumps the value of all the local variables:
main[1] locals
Method arguments:
Local variables:
j = 0
You can see that the value of j=0 is what caused the exception.
The wherei command prints the stack frames pushed in the method stack of the current thread:
main[1] wherei
[1] SimpleDebugging.foo3 (SimpleDebugging.java:18), pc = 15
[2] SimpleDebugging.foo2 (SimpleDebugging.java:11), pc = 8
[3] SimpleDebugging.foo1 (SimpleDebugging.java:6), pc = 8
[4] SimpleDebugging.main (SimpleDebugging.java:22), pc = 0
Each line after the wherei command represents a method call and the point where the call will return (which is shown by the value of the program counter pc). Here the calling sequence was main( ), foo1( ), foo2( ), and foo3( ). You can pop the stack frame pushed when the call was made to foo3( ) with the pop command:
main[1] pop
main[1] wherei
[1] SimpleDebugging.foo2 (SimpleDebugging.java:11), pc = 8
[2] SimpleDebugging.foo1 (SimpleDebugging.java:6), pc = 8
[3] SimpleDebugging.main (SimpleDebugging.java:22), pc = 0
You can make the JDB step through the call to foo3( ) again with the reenter command:
main[1] reenter
>
Step completed: "thread=main", SimpleDebugging.foo3(), line=15 bci=0
System.out.println("In foo3");
The list command shows us that the execution begins at the start of foo3( ):
main[1] list
11 foo3();
12 }
13
14 private static void foo3() {
15 => System.out.println("In foo3");
16 int j = 1;
17 j--;
18 int i = 5 / j;
19 }
20
JDB also allows you to modify the value of the local variables. The divide by zero that was caused by executing this piece of code the last time can be avoided by changing the value of j. You can do this directly in the debugger, so you can continue debugging the program without going back and changing the source file. Before you set the value of j, you will have to execute through line 25 since that is where j is declared.
main[1] step
> In foo3
Step completed: "thread=main", SimpleDebugging.foo3(), line=16 bci=8
16 int j = 1;
main[1] step
>
Step completed: "thread=main", SimpleDebugging.foo3(), line=17 bci=10
17 j--;
main[1] list
13
14 private static void foo3() {
15 System.out.println("In foo3");
16 int j = 1;
17 => j--;
18 int i = 5 / j;
19 }
20
21 public static void main(String[] args) {
22 foo1();
At this point, j is defined and you can set its value so that the exception can be avoided.
main[1] set j=6
j=6 = 6
main[1] next
>
Step completed: "thread=main", SimpleDebugging.foo3(), line=18 bci=13
18 int i = 5 / j;
main[1] next
>
Step completed: "thread=main", SimpleDebugging.foo3(), line=19 bci=17
19 }
main[1] next
>
Step completed: "thread=main", SimpleDebugging.foo2(), line=12 bci=11
12 }
main[1] list
8
9 private static void foo2() {
10 System.out.println("In foo2");
11 foo3();
12 => }
13
14 private static void foo3() {
15 System.out.println("In foo3");
16 int j = 1;
17 j--;
main[1] next
>
Step completed: "thread=main", SimpleDebugging.foo1(), line=7 bci=11
7 }
main[1] list
3 public class SimpleDebugging {
4 private static void foo1() {
5 System.out.println("In foo1");
6 foo2();
7 => }
8
9 private static void foo2() {
10 System.out.println("In foo2");
11 foo3();
12 }
main[1] next
>
Step completed: "thread=main", SimpleDebugging.main(), line=23 bci=3
23 }
main[1] list
19 }
20
21 public static void main(String[] args) {
22 foo1();
23 => }
24 } ///:~
main[1] next
>
The application exited
next executes a line at a time. You can see that the exception is avoided and we can continue stepping through the program. list is used to show the position in the program from where execution will proceed.