Chapter 9

Using the Debugger


CONTENTS


In this chapter you'll learn to use the Java debugger to trace and debug the Java programs you develop. You'll learn how to invoke the debugger, load class files, and examine classes as they are executed. You'll also explore the commands provided by the debugger and learn to use them through a hands-on tutorial. When you have finished this chapter, you will know how to use the debugger to analyze, test, and debug your Java programs.

Overview of the Debugger

The Java debugger enables Java programmers to debug their programs without having to insert special debugging instructions into their code. The debugger has a number of features, including support for multithreaded programs and remote applications.

The debugger is invoked with the jdb command. To get a quick summary of the commands provided by the debugger, enter the debugger command as follows:

C:\java\jdg>jdb
Initializing jdb...
>

Note
The Java debugger has a few bugs of its own. To get the debugger to run properly, you may have to establish an active Internet connection.

The debugger takes a few seconds to initialize and then provides you with the debugger prompt. At the debugger prompt, type help to get a description of the commands it supports:

C:\java\jdg>jdb
Initializing jdb...
> help
** command list **
threads [threadgroup]     -- list threads
thread <thread id>        -- set default thread
suspend [thread id(s)]    -- suspend threads (default: all)
resume [thread id(s)]     -- resume threads (default: all)
where [thread id] | all   -- dump a thread's stack
threadgroups              -- list threadgroups
threadgroup <name>        -- set current threadgroup

print <id> [id(s)]        -- print object or field
dump <id> [id(s)]         -- print all object information

locals                    -- print all local variables in current stack frame

classes                   -- list currently known classes
methods <class id>        -- list a class's methods

stop in <class id>.<method> -- set a breakpoint in a method
stop at <class id>:<line> -- set a breakpoint at a line
up [n frames]             -- move up a thread's stack
down [n frames]           -- move down a thread's stack
clear <class id>:<line>   -- clear a breakpoint
step                      -- execute current line
cont                      -- continue execution from breakpoint

catch <class id>          -- break for the specified exception
ignore <class id>         -- ignore the specified exception

list [line number]        -- print source code
use [source file path]    -- display or change the source path

memory                    -- report memory usage
gc                        -- free unused objects

load classname            -- load Java class to be debugged
run <class> [args]        -- start execution of a loaded Java class
!!                        -- repeat last command
help (or ?)               -- list commands
exit (or quit)            -- exit debugger
>

Learning the debugger involves learning how to use each of these commands.

An Extended Example

In order to get you quickly up to speed on the operation of the debugger, let's use it to analyze a program that you've already developed. Change directories to the ch06 directory and recompile the ch06 source files using the -g option. This will result in additional debugging information being inserted into the compiled bytecode files.

C:\java\jdg\ch06>javac -g CGTextEdit.java

C:\java\jdg\ch06>javac -g CGText.java

C:\java\jdg\ch06>javac -g CGTextPoint.java

C:\java\jdg\ch06>javac -g CGTextBox.java

C:\java\jdg\ch06>javac -g CDrawApp.java

When you have finished compiling the source files, run the debugger by entering the jdb command:

C:\java\jdg\ch06>jdb
Initializing jdb...
>

At the debugger prompt, type load jdg.ch06.CDrawApp:

> load jdg.ch06.CDrawApp
0x13a41b8:class(jdg.ch06.CDrawApp)
>

The debugger responds by loading the CDrawApp class. The hexadecimal number preceding the class name is a Java runtime identifier for the CDrawApp class. Load the CDraw class by typing load jdg.ch06.CDraw:

> load jdg.ch06.CDraw
0x13a54e8:class(jdg.ch06.CDraw)
>

Now that you've loaded these two classes, you want to set a breakpoint in the main() method of CDrawApp. A breakpoint is a place in your program where the debugger stops execution to allow you to enter debugging commands. You set a breakpoint using the stop in command:

> stop in CDrawApp.main
Breakpoint set in jdg.ch06.CDrawApp.main
>

This tells the debugger to stop execution when it encounters the main() method of CDrawApp. Because the main() method is the first method executed in the CDrawApp program, the debugger will stop just as it starts to execute CDrawApp. Run the debugger for CDrawApp to see how the breakpoint works:

> run CDrawApp
running ...
main[1]
Breakpoint hit: jdg.ch06.CDrawApp.main (CDrawApp:18)
main[1]

The debugger runs CDrawApp and stops at the breakpoint. It changes its prompt to main[1] to let you know that it is suspended in the number 1 stack frame of the main thread. A stack frame represents the state of the stack of the Java virtual machine as a result of a method invocation. Refer to the section "JVM Stack" in Chapter 37, "The Java Virtual Machine." Now that you've stopped the debugger with your breakpoint, use the list command to see where you are in the program's flow of control:

main[1] list
14         import java.io.IOException;
15
16         class CDrawApp {
17          public static void main(String args[]) throws IOException {
18      =>   CDraw program = new CDraw();
19           program.run();
20          }
21         }
22
main[1]

The arrow indicates that the debugger has stopped at the point where the program instance of CDrawApp is about to be created. Now step into the CDraw() constructor using the step command. This command allows you to control a program's execution one instruction at a time and is used to produce the following debugger output:

main[1] step
main[1]
Breakpoint hit: jdg.ch06.CDraw.<init> (CDraw:29)
main[1]

The debugger informs you that it has stopped at a breakpoint in the CDraw constructor. The <init> identifier is used to indicate a constructor. Enter another list command to see where you are:

main[1] list
25          static KeyboardInput kbd = new KeyboardInput(System.in);
26          BorderedPrintCGrid grid;
27
28          // Method declarations
29      =>  CDraw() {
30           grid = new BorderedPrintCGrid();
31          }
32          void run() throws IOException {
33           boolean finished = false;
main[1]

The debugger indicates that you're about to execute the CDraw() constructor. Skip forward in the program's execution until you reach the run() method of CDraw. Now set a breakpoint at the run() method:

main[1] stop in CDraw.run
Breakpoint set in jdg.ch06.CDraw.run
main[1]

Now continue running the debugger with the continue command:

main[1] cont
main[1]
Breakpoint hit: jdg.ch06.CDraw.run (CDraw:33)
main[1]

The debugger indicates that it stopped at your breakpoint. Use the list command to see where the debugger stopped:

main[1] list
29          CDraw() {
30           grid = new BorderedPrintCGrid();
31          }
32          void run() throws IOException {
33      =>   boolean finished = false;
34           do {
35            char command = getCommand();
36            switch(command){
37             case 'P':
main[1]

You're at the first instruction in the run() method. Let's take a little break here and look around a bit. First, use the methods command to list the methods that are available to the CDraw class:

main[1] methods CDraw
void <init>()
void run()
char getCommand()
void addPoint()
void addBox()
void addText()
void editText()
void editText(CGTextEdit)
void <clinit>()
main[1]

The debugger responds by listing all methods declared for CDraw, including its constructors, <init> and <clinit>. These constructors are internal methods generated by the Java virtual machine.

The classes command lists all classes that are currently known (loaded) by the debugger. Let's take a look at them:

main[1] classes
** classes list **
0x1393008:class(java.lang.Thread)
0x1393018:class(java.lang.Object)
0x1393098:class(java.lang.Class)
0x1393028:class(java.lang.String)
0x1393038:class(java.lang.ThreadDeath)
0x1393048:class(java.lang.Error)
0x1393058:class(java.lang.Throwable)
0x1393068:class(java.lang.Exception)
0x1393078:class(java.lang.RuntimeException)
0x1393088:interface(java.lang.Cloneable)
0x13930b0:class(java.lang.ThreadGroup)
0x13930e0:class(java.lang.System)
0x13930f0:class(java.io.BufferedInputStream)
0x1393100:class(java.io.FilterInputStream)
0x1393110:class(java.io.InputStream)
0x1393128:class(java.io.FileInputStream)
0x1393140:class(java.io.FileDescriptor)
0x1393170:class(java.io.PrintStream)
0x1393180:class(java.io.FilterOutputStream)
0x1393190:class(java.io.OutputStream)
0x13931a8:class(java.io.BufferedOutputStream)
0x13931c0:class(java.io.FileOutputStream)
0x1393208:class(java.lang.StringBuffer)
0x1393240:class(java.lang.Integer)
0x1393250:class(java.lang.Number)
0x13932a8:class(java.lang.NoClassDefFoundError)
0x13932b8:class(java.lang.LinkageError)
0x13932c8:class(java.lang.OutOfMemoryError)
0x13932d8:class(java.lang.VirtualMachineError)
0x13932f0:class(sun.tools.debug.EmptyApp)
0x1393300:class(sun.tools.debug.Agent)
0x1393328:class(java.lang.Runtime)
0x1393370:class(java.util.Properties)
0x1393380:class(java.util.Hashtable)
0x1393390:class(java.util.Dictionary)
0x13933a8:class(java.util.HashtableEntry)
0x1393768:class(java.net.ServerSocket)
0x1393780:class(java.net.PlainSocketImpl)
0x1393790:class(java.net.SocketImpl)
0x13937e0:class(java.net.InetAddress)
0x13938a8:class(java.lang.Math)
0x13938b8:class(java.util.Random)
0x1393948:class(java.lang.Character)
0x1393a18:class(sun.tools.java.ClassPath)
0x1393a28:class(java.lang.Compiler)
0x1393a58:class(java.io.File)
0x1393aa0:class(sun.tools.java.ClassPathEntry)
0x1393b10:class(sun.tools.zip.ZipFile)
0x1393b40:class(java.io.RandomAccessFile)
0x1393bb0:interface(sun.tools.zip.ZipConstants)
0x1393c00:class(sun.tools.zip.ZipEntry)
0x13a2638:class(sun.tools.debug.BreakpointHandler)
0x13a2670:class(sun.tools.debug.BreakpointQueue)
0x13a26a8:class(java.util.Vector)
0x13a26c8:class(java.net.Socket)
0x13a28f0:class(java.io.DataInputStream)
0x13a2910:class(java.net.SocketInputStream)
0x13a2938:class(sun.tools.debug.ResponseStream)
0x13a2950:class(java.net.SocketOutputStream)
0x13a2978:class(java.io.DataOutputStream)
0x13a29e8:class(sun.tools.debug.AgentOutputStream)
0x13a2ab0:class(java.util.HashtableEnumerator)
0x13a2ad8:class(java.util.VectorEnumerator)
0x13a2f48:interface(java.lang.Runnable)
0x13a37d0:interface(sun.tools.debug.AgentConstants)
0x13a3d18:interface(java.io.DataOutput)
0x13a3d28:interface(java.io.DataInput)
0x13a4130:interface(java.util.Enumeration)
0x13a41b8:class(jdg.ch06.CDrawApp)
0x13a44e0:interface(sun.tools.java.Constants)
0x13a4508:class(sun.tools.java.Identifier)
0x13a54e8:class(jdg.ch06.CDraw)
0x13a54f8:class(jdg.ch05.KeyboardInput)
0x13a5810:interface(sun.tools.java.RuntimeConstants)
0x13a6bf8:class(java.lang.ClassNotFoundException)
0x13a6fc8:class(sun.tools.debug.Field)
0x13a7a38:class(sun.tools.debug.BreakpointSet)
0x13a7dc0:class(sun.tools.debug.MainThread)
0x13a8090:class(sun.tools.debug.StackFrame)
0x13a8168:class(sun.tools.java.Package)
0x13a8230:class(sun.tools.java.ClassFile)
0x13a8318:class(sun.tools.debug.LineNumber)
0x13a9830:class(jdg.ch05.BorderedPrintCGrid)
0x13a9840:class(jdg.ch05.PrintCGrid)
0x13a9850:class(jdg.ch05.CGrid)
0x13a9868:class([[C)
0x13a9878:class([C)
0x13a9930:class(jdg.ch05.CGObject)
main[1]

That's quite a number of classes! Look through this list to see if there are any that you recognize. You should be able to identify some classes that are used by the CDrawApp program.

The threadgroups command lists the threadgroups that are currently defined by the program:

main[1] threadgroups
1. (java.lang.ThreadGroup)0x13930b8 system
2. (java.lang.ThreadGroup)0x13939c0 main
3. (java.lang.ThreadGroup)0x13a7d60 jdg.ch06.CDrawApp.main
main[1]

The three threadgroups are the system threadgroup (used by the Java runtime system), the default main threadgroup, and the threadgroup associated with the CDrawApp program.

The threads command tells you what threads are in a threadgroup:

main[1] threads system
Group system:
 1. (java.lang.Thread)0x13931f8                  Finalizer thread
 2. (java.lang.Thread)0x1393918                  Debugger agent
 3. (sun.tools.debug.BreakpointHandler)0x13a2640 Breakpoint handler
Group main:
 4. (java.lang.Thread)0x13930a0 main suspended
Group jdg.ch06.CDrawApp.main:
 5. (sun.tools.debug.MainThread)0x13a7dc8 main at breakpoint
main[1]

When you list the threads in the system threadgroup, you get a list of all threads maintained by the Java runtime system.

The memory command tells you how much memory is available to the Java runtime system:

main[1] memory
Free: 2439408, total: 3145720
main[1]

The available memory on your computer may differ from mine. For your information, I'm currently running Java on a 486/DX-2 66 computer with 20MB of RAM. Obviously, Java isn't using all of the memory that's available to it.

The where command dumps the stack used by the Java virtual machine. It displays the current list of methods that have been invoked to get you to your breakpoint. An example of the where command follows:

main[1] where
  [1] jdg.ch06.CDraw.run (CDraw:33)
  [2] jdg.ch06.CDrawApp.main (CDrawApp:19)
main[1]

The where command comes in handy when you are deep in the inner layers of several nested method invocations. It shows you how you got to where you are within the program.

You can use the up and down commands to move up and down the stack. The up command moves you to a higher stack frame within the stack:

main[1] up

main[2]

Do a list command to see the results of the up command:

main[2] list
15
16         class CDrawApp {
17          public static void main(String args[]) throws IOException {
18           CDraw program = new CDraw();
19      =>   program.run();
20          }
21         }
22
23         public class CDraw {
main[2]

Now use the down command to go back down the stack to where you were before:

main[2] down
main[1]

Do another list command to verify that you have returned to where you were before you entered the up command:

main[1] list
29          CDraw() {
30           grid = new BorderedPrintCGrid();
31          }
32          void run() throws IOException {
33      =>   boolean finished = false;
34           do {
35            char command = getCommand();
36            switch(command){
37             case 'P':
main[1]

Now let's look at some variables. Enter the locals command to get a list of local variables of the run() method:

main[1] locals
Local variables and arguments:
  this = jdg.ch06.CDraw@13a7ce8
  finished is not in scope.
  command is not in scope.
main[1]

The finished and command variables are not in the current scope because they have not yet been declared. Step over to the next statement:

main[1] step
main[1]
Breakpoint hit: jdg.ch06.CDraw.run (CDraw:35)
main[1]

Enter the list command to see where you have stepped:

main[1] list
31          }
32          void run() throws IOException {
33           boolean finished = false;
34           do {
35      =>    char command = getCommand();
36            switch(command){
37             case 'P':
38               addPoint();
39              System.out.println();
main[1]

Do another locals command. The finished variable should now be in scope:

main[1] locals
Local variables and arguments:
  this = jdg.ch06.CDraw@13a7ce8
  finished = false
  command is not in scope.
main[1]

You have now covered most of the debugger commands. Now let's go on to debugging multithreaded programs. Type exit to exit the debugger.

Debugging Multithreaded Programs

The Java debugger supports the debugging of multithreaded programs. In fact, it provides a great tool for understanding how multithreaded programs work. In this section, you use the debugger to debug the ThreadTest1 program that you developed in Chapter 8, "Multithreading."

Change directories to the ch08 directory and enter javac -g ThreadTest1.java to add additional debugging information to the ThreadTest1.class bytecode file:

C:\java\jdg\ch08>javac -g ThreadTest1.java

C:\java\jdg\ch08>

Now start jdb and load ThreadTest1 with the command jdb ThreadTest1:

C:\java\jdg\ch08>jdb ThreadTest1
Initializing jdb...
0x13a41b8:class(ThreadTest1)
>

Set a breakpoint at the main() method of ThreadTest1:

> stop in ThreadTest1.main
Breakpoint set in ThreadTest1.main
>

Run ThreadTest1:

> run ThreadTest1
running ...

Breakpoint hit: ThreadTest1.main (ThreadTest1:9)
main[1]

The debugger runs ThreadTest1 and stops at your breakpoint. Do a list command to see where the debugger stopped:

main[1] list
5          import java.lang.InterruptedException;
6
7          class ThreadTest1 {
8           public static void main(String args[]) {
9       =>   MyThread thread1 = new MyThread("thread1: ");
10           MyThread thread2 = new MyThread("thread2: ");
11           thread1.start();
12           thread2.start();
13           boolean thread1IsAlive = true;
main[1]

The debugger is at the beginning of the main() method. It has not created any new threads at this time. Use the threads command to verify this:

main[1] threads
Group ThreadTest1.main:
 1. (sun.tools.debug.MainThread)0x13a5d88 main at breakpoint
main[1]

The only thread is the current main thread of execution. Set a breakpoint to line 11 of ThreadTest1, the point where both thread1 and thread2 will be declared:

main[1] stop at ThreadTest1:11
Breakpoint set at ThreadTest1:11
main[1]

Now jump to that point in the program:

main[1] cont
main[1]
Breakpoint hit: ThreadTest1.main (ThreadTest1:11)
main[1]

Use the threads command again to see the effect of the thread1 and thread2 declarations:

Group ThreadTest1.main:
 1. (sun.tools.debug.MainThread)0x13a5d88 main      at breakpoint
 2. (MyThread)0x13a6b70                   thread1:  zombie
 3. (MyThread)0x13a6b98                   thread2:  zombie
main[1]

Both thread1 and thread2 are in the New Thread state. The debugger refers to them as zombies. That's a curious term considering that the threads have neither started nor died at this point in the program's execution.

Now jump ahead in the program to line 13, where both threads are started. First, set the breakpoint:

main[1] stop at ThreadTest1:13
Breakpoint set at ThreadTest1:13
main[1]

Now jump ahead to the breakpoint:

main[1] cont

Breakpoint hit: ThreadTest1.main (ThreadTest1:13)
main[1]

Let's take a quick look around to make sure you are where you want to be:

main[1] list
9            MyThread thread1 = new MyThread("thread1: ");
10           MyThread thread2 = new MyThread("thread2: ");
11           thread1.start();
12           thread2.start();
13      =>   boolean thread1IsAlive = true;
14           boolean thread2IsAlive = true;
15           do {
16            if(thread1IsAlive && !thread1.isAlive()){
17              thread1IsAlive = false;
main[1]

You should now get different results when you execute the threads command:

Group ThreadTest1.main:
 1. (sun.tools.debug.MainThread)0x13a5d88 main      at breakpoint
 2. (MyThread)0x13a6b70                   thread1:  suspended
 3. (MyThread)0x13a6b98                   thread2:  running
main[1]

Note
Depending on the machine on which you run the jdb, you may find that both threads are suspended when you execute the threads command.

The debugger tells us that thread1 is suspended and thread2 is running. The suspend command is used to suspend the execution of a running thread. It takes the number of the thread identified by the threads command as its argument. The suspend command is used as follows:

main[1] suspend 3
main[1]

Use the threads command to verify that it works:

main[1] threads
Group ThreadTest1.main:
 1. (sun.tools.debug.MainThread)0x13a5d88 main      at breakpoint
 2. (MyThread)0x13a6b70                   thread1:  suspended
 3. (MyThread)0x13a6b98                   thread2:  suspended
main[1]

Now switch threads to thread1 using the thread command:

main[1] thread 2
thread1: [1]

Notice how the prompt changed to indicate that you switched to thread1. Let's do a list command to see where we are in thread1. The results of the list command follow:

thread1: [1] list
36            System.out.println(name+message[i]);
37           }
38          }
39          void randomWait(){
40      =>   try {
41            sleep((long)(3000*Math.random()));
42           }catch (InterruptedException x){
43            System.out.println("Interrupted!");
44           }
thread1: [1]

Thread1 is in the middle of the randomWait() method.

Switch threads to see what thread2 is up to:

thread2: [1] list
36            System.out.println(name+message[i]);
37           }
38          }
39          void randomWait(){
40      =>   try {
41            sleep((long)(3000*Math.random()));
42           }catch (InterruptedException x){
43            System.out.println("Interrupted!");
44           }
thread2: [1]

It looks like thread2 is in the same state as thread1.

Set a breakpoint for thread1 and thread2:

thread2: [1] stop at MyThread:36
Breakpoint set at MyThread:36
thread2: [1] thread 2
thread1: [1] stop at MyThread:36
Breakpoint set at MyThread:36
thread1: [1]

Now continue the execution of thread1:

thread1: [1] cont
thread1: [1] list
32          public void run() {
33           String name = getName();
34           for(int i=0;i<message.length;++i) {
35            randomWait();
36      =>    System.out.println(name+message[i]);
37           }
38          }
39          void randomWait(){
40           try {
thread1: [1]

The thread executes up to the breakpoint. You can verify this by running the threads command:

thread1: [1] threads
Group ThreadTest1.main:
 1. (sun.tools.debug.MainThread)0x13a5888 main      suspended
 2. (MyThread)0x13a59f0                   thread1:  at breakpoint
 3. (MyThread)0x13a5a18                   thread2:  suspended
thread1: [1]

If you use the step command, thread1 becomes suspended and thread2 reaches the breakpoint:

thread1: [1] step
thread1: [1]
Breakpoint hit: MyThread.run (MyThread:36)
thread2: [1] threads
Group ThreadTest1.main:
 1. (sun.tools.debug.MainThread)0x13a5888 main      suspended
 2. (MyThread)0x13a59f0                   thread1:  suspended
 3. (MyThread)0x13a5a18                   thread2:  at breakpoint
thread2: [1]

You can use the print and dump commands to display the values of the message field of MyThread:

thread2: [1] print MyThread.message
"MyThread" is not a valid field of (MyThread)0x13a5a18
MyThread.message = 0x13a5958 Object[6] = { Java, is, hot,,     }
thread2: [1] dump MyThread.message
"MyThread" is not a valid field of (MyThread)0x13a5a18
MyThread.message = 0x13a5958 Object[6] = { Java, is, hot,,     }
thread2: [1]

These commands are somewhat buggy. They complain that the fields are not valid, but they display the values of the fields anyway.

At this point, you've covered all the important features of the Java debugger. You can experiment with the debugger to see how the two threads continue their execution. When you are finished, use the exit command to terminate the debugger.

Summary

In this chapter you have learned how to use the Java debugger to step through the execution of a Java program. You have learned how to invoke the debugger, load class files, and examine classes as they are executed. In Chapter 10, "Automating Software Documentation," you will learn how to use another program contained in the Java toolkit-the Java documentation tool. You'll see how this tool can help you to quickly and easily develop documentation for your Java programs.