JDB Debugging Tutorial
By:
For:
Last Modified: 18 October 2005
Part 1
Contents
This tutorial is designed to show how to use JDB (the Java
Debugger) to find and fix errors in Java programs. It assumes no
prior knowledge of debugging tools or techniques, and so should be most
useful to programming beginners. Any programmer who has not had
experience with a command-line debugger (or with any other debugger,
for that matter) may find it useful, however.
The tutorial is split into two parts. Part 1 (this document)
describes the basics of debugging and includes a lot of conceptual
detail behind the most important JDB commands. covers a wider range of commands, but
offers less detail for the rationale behind them.
Any debugger, whether it be JDB running at the command prompt or a
graphical debugger running in a fancy development environment, is
designed to let you do two basic things:
Trace Execution. You can execute one line of your program at a
time, so you'll be able to see exactly what code is executing and find
exactly which lines are causing problems.
Watch Variables. Look at what values all your variables have as
the code executes so you can see which ones have incorrect values and
where in the code they got those values.
A variety of tools inside JDB can help accomplish these things.
That's what this tutorial explains.
Before we begin, it's important to distinguish between the two
kinds of errors you can have in a program. Compiler errors occur when
you first compile your source code, and identify basic syntactic
mistakes. Believe it or not, they're the easy ones to fix. If javac
says Program.java:15: ';' expected you know that
somewhere on or around line 15 there's a problem. Even if it's not
really a missing semicolon at least you know where to look.
The errors that are hard to fix are the kind that occur as your
program is running. You get it to compile without any errors, but
when it runs it doesn't behave as you expect. Those are called
"runtime" errors, and the debugger exists to help correct them.
Let's start by looking at an extremely simple program that has a
very simple bug, just to get the hang of this. Mosquito is a
program that's supposed to get two numbers from the user, multiply
them, and display the result. This program "compiles clean" (no
errors, no warnings) but it does not behave as expected. Here's an
example of what it does:
Prompt for the numbers:
Display the result:
Sample Run of Mosquito
Clearly something is wrong, even though it compiled without any
trouble. First, the numbers it's supposedly multiplying aren't the
numbers I entered. Second, the product of 2 and 0 is not 2.
The code for Mosquito is below. Copy and paste it into a new
file, compile it, and run it to see how it works. You can probably
identify the bugs just by looking at the code, but leave them there.
The point is to see how the debugger can help.
/** Mosquito: Buggy Program #1
* Prompts the user for two numbers and multiplies them.
* Demonstrating Basic Debugger Tools
* Debugger Tutorial, 6 February 2004
*/
import javax.swing.JOptionPane;
/**
* @author Benjamin.Fenster@uvm.edu
* @version 19 January 2004
*/
class Mosquito {
public static void main(String[] args) {
int num1 = 0;
int num2 = 0;
int result = 0;
// Get two numbers from the user
num1 = getIntegerInput("Enter the first number", "Mosquito");
num1 = getIntegerInput("Enter the second number", "Mosquito");
// Calculate the product of those two numbers
result = num1 + num2;
// Provide the result for the user
JOptionPane.showMessageDialog(null,"" + num1 + " x " + num2 + " = " + result, "Mosquito", JOptionPane.INFORMATION_MESSAGE);
// And we're done!
System.exit(0);
}
/** Prompts the user for a number and returns the result as an integer.
* Pre: 'message' is the message to be displayed in the dialog box
* 'title' is the title of the dialog box
* Post: The value entered by the user is returned as an integer.
*/
public static int getIntegerInput(String message, String title) {
String temp = JOptionPane.showInputDialog(null, message, title, JOptionPane.QUESTION_MESSAGE);
return Integer.parseInt(temp);
}
}
First, compile your program with javac -g Mosquito.java.
The "-g" option tells the compiler to include extra information the
debugger is going to use. If you forget and compile the program
normally, some debugger functions will still work but others
(like looking at variables' values) will not.
Next, instead of running your program with the java
command, start the debugger by typing jdb Mosquito.
H:\>javac -g Mosquito.java
H:\>jdb Mosquito
Initializing jdb ...
>
Notice that the DOS prompt (H:\> in this example)
is gone, and just a > prompt is left. This is the JDB
prompt where you'll type debugging commands. For example, type
help to get a list of possible commands. Some of the
commands listed will make absolutely no sense, but others will sound
perfectly reasonable. We'll explore the most important ones in this
tutorial.
The whole idea behind the debugger is to execute only part of your
program at a time. Otherwise you'd just run it the usual way. The
first thing we need to do, then, is tell the debugger to "break" (stop
executing and wait for your command) at some point in the program.
That, logically enough, is called a "breakpoint."
You can put a breakpiont on any executable line of Java code (so
not a comment or something like an import statement, but any line of
code that actually does something). To start, let's just put a
breakpoint right at the beginning of main() by typing stop in
Mosquito.main.
>stop in Mosquito.main
Deferring breakpoint Mosquito.main.
It will be set after the class is loaded.
>
In so many words, the debugger has agreed to set that breakpoint
just as soon as you start running the program. Without further ado,
then, let's run the program so it can get on with it. The command,
shockingly, is run.
>run
run Mosquito
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
>
VM Started: Set deferred breakpoint Mosquito.main
Breakpoint hit: "thread=main", Mosquito.main(), line=14 bci=0
14 int num1 = 0;
main[1]
Yep, that's a lot of junk to interpret. You can ignore the two
lines about Set uncaught java.lang.Throwable. There's a
> prompt next, but you don't get a chance to type
anything there so don't worry about that either.
Next, after "VM Started," the debugger confirms that it has set a
breakpoint in main() like you asked. That's simple enough, although
really not very exciting.
Finally, then, we get to the point where something interesting
happens: the breakpoint is hit. The useful part of that line says
Mosquito.main(), line 14. That's telling you which
statement in your program will execute next. It even shows you the
statement itself on the next line:
14 int
num1 = 0;
Now, just to make things confusing, the prompt changes again. Gone
is the recognizable > prompt and in its place you get
main[1]. That might at least have made sense if "main"
referred to the method currently running, but it doesn't. There is a
reason for it, but for now don't worry about it. Just note that
you've got a new prompt.
We can see just looking at it that int num1 = 0 is a
pretty boring statement. Let's just execute it and get it over with.
How? Type next.
main[1] next
>
Step completed: main[1] "thread=main", Mosquito.main(), line=15 bci=2
15 int num2 = 0;
You've asked jdb to execute one line of code and move on to the
next. You again see the next line of code to execute. That's
important. You're looking at the next line of code to
execute. That will confuse you as you debug your own (more complex)
programs, since it may seem like it's showing the code it just
executed. It's always showing what will happen next.
Fine, so that means num1 is now officially declared and should have
a value of zero. Since that's pretty dull go ahead and execute the
next two lines too:
main[1] next
>
Step completed: "thread=main", Mosquito.main(), line=16 bci=4
16 int result = 0;
main[1] next
>
Step completed: "thread=main", Mosquito.main(), line=19 bci=6
19 num1 = getIntegerInput("Enter the first number", "Mosquito
");
Now we see line 19 ready to execute, and it seems to do something a
little more interesting than just declaring a variable. Before you
execute an interesting line of code, make a prediction about what it
will do. Then, when it's done running, make sure your prediction was
right. In this case, I predict, "I'll type 34, so num1 will be 34
when I'm done."
When you type next, two things happen. First, that
funny > prompt comes up again with a blinking cursor.
Then the JOptionPane shows up on the screen. Just to annoy you,
however, if you start typing it'll show up at the >
prompt, not in the JOptionPane. Use Alt+Tab or click the JOptionPane
to select it and then type your number. If you don't see the
JOptionPane at all, use Alt+Tab to find it (it won't be in
your taskbar).
main[1] next
>
Step completed: "thread=main", Mosquito.main(), line=20 bci=14
20 num1 = getIntegerInput("Enter the second number", "Mosquit
o");
main[1]
Great. So the statement executed. Now it's time to check whether
your prediction ("num1 will be 34") is correct. Ask what the value of
num1 is by using the print command:
main[1] print num1
num1 = 34
Finally we find a command with output that doesn't require
clarification! What's more, the results match our prediction so we
know there's nothing wrong with the line of code that just
executed.
Here's the secret to debugging a program. Every
time you execute a statement, make a prediction about what it will
do. Will a variable's value change? Will you run a method? Will the
'if' statement happen or not? Then, when you execute it, see whether
your prediction was right or not.
If your prediction was right, then the program did exactly what you
thought it should, and there's no point wasting time examining that
statement. If everything in your program worked the way you
thought it did you wouldn't have a bug! On the other hand, if your
prediction is wrong, you've just found a problem that needs to be solved.
My prediction about the next line of code (line 20) is that num1
will still be 34 and num2 will be 2, because I intend to enter 2 when
prompted.
main[1] next
>
Step completed: "thread=main", Mosquito.main(), line=23 bci=22
23 result = num1 + num2;
main[1] print num2
num2 = 0
Note that this time the JOptionPane gets focus right away, so you
don't have to click on it. The first time you use a JOptionPane it
won't get focus at first. Every time after that it will. That'll
annoy you to death.
Clearly, we see that the prediction was wrong. num2 is still 0.
Now, how do you figure out what did happen? Start by taking
a look at all the variables. No, don't just print them
all. Use the locals command instead:
main[1] locals
Method arguments:
args = instance of java.lang.String[0] (id=1243)
Local variables:
num1 = 2
num2 = 0
result = 0
You get separate lists of any arguments to the current method
(which is just 'args' in this case since we're in main()) and ordinary
variables just declared somewhere inside the method. We can see
immediately that the 2 I had wanted to end up in num2 actually ended
up in num1.
Well, we know we just executed a problematic line of code. You may
find it convenient to see where that line is in context before going
back to your editor to make any changes. Try the list
command:
main[1] list
19 num1 = getIntegerInput("Enter the first number", "Mosquito
");
20 num1 = getIntegerInput("Enter the second number", "Mosquit
o");
21
22 // Calculate the product of those two numbers
23 => result = num1 + num2;
24
25 // Provide the result for the user
26 JOptionPane.showMessageDialog(null,"" + num1 + " x " + num
2 + " = " + result, "Mosquito", JOptionPane.INFORMATION_MESSAGE);
27
28 // And we're done!
Note the => arrow pointing to the next statement to
be executed. Now go back to the editor and change it to num1 =
.... Recompile the program and run it again without
the debugger. Assume your change worked and test it. If it doesn't
work, come back and try again with the debugger.
Don't forget to type exit to quit jdb.
We have to add one little twist when it comes to running methods.
Notice that the getIntegerInput() function just executed from top to
bottom without waiting for you to keep typing "next" for each
statement. What if your bug were in that method? Let's debug the
program again, this time so that we get to step through each statement
in getIntegerInput().
This time, set a breakpoint just before it calls getIntegerInput()
on line 19 by typing stop on Mosquito:19. Then
run the program and see the breakpoint come up.
> stop on Mosquito:19
Deferring breakpoint Mosquito:19.
It will be set after the class is loaded.
> run
run Mosquito
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
>
VM Started: Set deferred breakpoint Mosquito:19
Breakpoint hit: "thread=main", Mosquito.main(), line=19 bci=6
19 num1 = getIntegerInput("Enter the first number", "Mosquito
");
Now, suppose you've been stepping through some big program and come
to this line where getIntegerInput() function is called. You know
there may be a problem inside getIntegerInput() so you'd like to see
those statements executed individually. If you type next
it'll just call the function, do everything it says to do, and then
come back. Instead, type step.
main[1] step
>
Step completed: "thread=main", Mosquito.getIntegerInput(), line=39 bci=0
39 String temp = JOptionPane.showInputDialog(null, message, t
itle, JOptionPane.QUESTION_MESSAGE);
Sure enough, we're now inside the method. Use the list
to get some context if you want. Now you can go back to using
next and the other commands we've seen to explore this
method. If you ever step into a method and then realize it was a
mistake, use step up to execute the rest of the method and
stop when it's done. (It steps UP to the calling function.)
These features (setting breakpoints, stepping over one line of
code, stepping into a method, and looking at variables' values) are
the basic features you should expect to find in any debugger. They're
also the ones you'll use most often. There are other debugger
commands that aren't necessarily "mainstream" but which are exactly
the right tools for particular situations
If you've mastered the basics of debugging, move on to and see more of what JDB can do for
you. If you're still getting used to these basic commands, don't just
dive in head first to the advanced commands. Wait until you've gotten
the basics figured out and then come back.
Part 2
Contents
Go back to of this tutorial, if you
haven't already, and compile the Mosquito program. If
you're new to using the debugger, you may want to walk through the
entire first part before proceeding with these new commands.
You may have noticed that there's a lot of repetition in a
command-line debugger. Every time you want to check up on a variable
you have to type print variable, for example. If you're
watching a bunch of variables over lots of lines of code, that can be
a huge hassle.
Fortunately, JDB offers a feature called a monitor,
which is just a command that gets executed every time the debugger
stops. That means every time you hit a breakpoint and every time you
step over a line of code (or into a method) your command will get
run.
For example, type monitor print num and then try
executing some statements with the next command. The
print num command gets executed automatically each time
the program stops:
> run
run Mosquito
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
>
VM Started: Set deferred breakpoint Mosquito.main
Breakpoint hit: "thread=main", Mosquito.main(), line=14 bci=0
14 int num1 = 0;
main[1] monitor print num1
main[1] next
>
Step completed: "thread=main", Mosquito.main(), line=15 bci=2
15 int num2 = 0;
main[1] num1 = 0
main[1] next
>
Step completed: "thread=main", Mosquito.main(), line=16 bci=4
16 int result = 0;
main[1] num1 = 0
main[1] next
>
Step completed: "thread=main", Mosquito.main(), line=19 bci=6
19 num1 = getIntegerInput("Enter the first number", "Mosquito
");
main[1] num1 = 0
main[1] next
>
Step completed: "thread=main", Mosquito.main(), line=20 bci=14
20 num1 = getIntegerInput("Enter the second number", "Mosqui
o");
main[1] num1 = 34
main[1] next
>
Step completed: "thread=main", Mosquito.main(), line=23 bci=22
23 result = num1 + num2;
main[1] num1 = 2
Note that the num1 = ... outputs appear next to the
main[1] prompt. That's just where they appear; it's not
anything I typed.
You can monitor any command you like (you might monitor
list, for example, if you find the single line of source
code isn't helpful enough). You can just type monitor by
itself to see what monitors you have right now, and use unmonitor
# with the number listed to turn it off:
main[1] monitor
1: print num1
main[1] unmonitor 1
Unmonitoring 1: print num1
Being able to have a command execute automatically is nice, but is
still insufficient if you don't even know which region of code is
responsible for "breaking" a variable.
Consider another program called Wasp. This one doesn't really have
a bug, per se. It doesn't even have a clear purpose. It constructs a
Wasp object, and then at some point in the future (in the middle of
the update() method after 100 iterations of a loop) it updates a class
variable. We'll suppose this particular change to the variable is not
one we expect.
Wasp.java
/** Wasp: Buggy Program #2
* Mutates a simple state variable
* Demonstrating Watchpoints
* Debugger Tutorial, 6 February 2004
*/
/** Wasp
* @author Benjamin Fenster
* @version 23 January 2004
*/
class Wasp {
public int state;
public Wasp() {
state = 5;
}
public static void main(String[] args) {
Wasp w = new Wasp();
w.update();
}
public void update() {
for (int i = 0; i < 1000; i++) {
if (i == 100) {
System.out.println(state);
state = 7;
}
}
}
}
This may seem a little contrived, but the idea that a variable
would get messed up in the middle of a huge loop is entirely
realistic. It would also be utterly painful to have to do
next 100 times every single time you wanted to test it in
the debugger.
Start JDB with the Wasp program ( jdb Wasp ) and tell it
to watch the state variable by typing the watch
Wasp.state command. Then run the program (you do not
need to set any breakpoints: that's the whole point of this).
H:\>jdb Wasp
Initializing jdb ...
> watch Wasp.state
Deferring watch modification of Wasp.state.
It will be set after the class is loaded.
> run
run Wasp
>
VM Started: Set deferred watch modification of Wasp.state
Field (Wasp.state) is 0, will be 5: "thread=main", Wasp.(), line=16 bci=6
16 state = 5;
main[1]
Just like when we set a breakpoint upon first starting the
debugger, it now defers setting the watch. Upon typing run
it starts running and doesn't stop until just before the
variable gets changed.
That's an important thing to understand. Just as a breakpoint
causes the debugger to stop before executing the statement on
that line, a watch causes the debugger to stop before the
variable is modified. The language in the output makes this clear:
Field (Wasp.state) is 0, will be 5.
Use the list command (and any others you may find
helpful) to determine where you are in the code and whether this
particular change to the variable is important or not.
Already in the output from when the watch was hit we can see that
it's in Wasp.() (where is just
the constructor). This is a change I expect, so I want to keep going.
Type cont to continue execution. (If you accidentally type
run, you'll just get an error telling you to use
cont.)
main[1] cont
5
>
Field (Wasp.state) is 5, will be 7: "thread=main", Wasp.update(), line=28 bci=24
28 state = 7;
Now, perhaps, you'd be surprised to see state changing
in the update() method. You could again use
list and print to learn more:
main[1] list
24 public void update() {
25 for (int i = 0; i < 1000; i++) {
26 if (i == 100) {
27 System.out.println(state);
28 => state = 7;
29 }
30 }
31 }
32 }
33
main[1] print i
i = 100
main[1]
Of course, since this program has no particular purpose it would be
silly to talk about what you might infer from this information, but
when looking at a real piece of code you'd presumably find something
askew that has caused state to change at this unexpected
point in the code.
For all JDB's great features, there are a few key areas it's not
very strong. For example, it would be nice to have it print out the
entire call stack (i.e. the list of functions currently executing:
main() called func1() called func2() called...) so you could see how
you got to where you are (particularly if you call the same method
from multiple locations. JDB doesn't offer such a feature, but other
debuggers definitely do.
The power of JDB is largely its ubiquity. Everywhere there's a
Java compiler there ought to be JDB, so even if you don't have access
to a full-featured development environment you can still use some
powerful debugging tools. A development environment like
(which is free) will contain a debugger that does everything JDB does,
and usually more.