Chapter 15

jdb: The Java Debugger


CONTENTS


Debugging is one of the necessary "evils" of software development. Very few developers on earth can routinely write large segments of error-free code on the first try. What often separates good developers from great developers is their skill at debugging complex code. To be considered truly masterful in a programming environment or language, a programmer must first master that environment's complete debugging capabilities. Modern programming environments allow the developer to set breakpoints, view function call stacks, and watch variables as program code is stepped through. This chapter will address using the JDK's debugger, jdb, so that you can take advantage of its debugging capabilities. If you are not already familiar with jdb, be sure to thoroughly cover the material in this chapter and work through the examples.

The Java Debugger API

jdb is the name of the debugger supplied by Sun in the Java Developer's Kit (JDK). It is a command-line tool (like the interpreter, compiler, and applet viewer) that you can use to step through Java code and study the code's behavior at runtime.

Warning
At an early stage, avoid the temptation to ignore the jdb debugging tool! Although average developers are relying on print statements to the screen to examine the contents of variables, jdb allows the above-average developer to step through code line by line and examine that code's operation. This is an invaluable resource that you should not bypass.

jdb was designed to have some special capabilities not found in many other language debuggers. These include:

The debugger was implemented using the Java Debugger API. This API was provided by Sun in order to fulfill these requirements for a full-featured Java language debugger. A key feature of the Debugger API is the capability of remote debugging. This means that a remote viewer can see into a Java Language Runtime if the security precautions have been taken care of.

The Sun documentation stresses that jdb was implemented as a "proof-of-concept" tool for the Java Debugger API. In other words, they viewed the real product as the API itself. (Tool developers were strongly encouraged to produce a more user-friendly tool.) More user-friendly debuggers currently exist in the form of the Symantec Café debugger and in the forthcoming Borland Latte IDE's debugger.

Security Precautions

The ability for programmers to debug Java applications and applets from remote locations is an exciting concept. Experienced developers realize that just because an application may run fine on their development platform, that does not mean it will run without flaws on other users' machines. In the Windows environment, common distribution problems include conflicting DLLs, VBXs, and OCXs, as well as an extremely large assortment of software packages that may conflict with changes being made to the system. The ability to remotely debug applications on users' machines is extremely powerful; however, it also introduces a variety of potential security problems.

One potential security "hole" could occur in the following situation. User A could be running a Java application locally on his machine. Using this application, he is entering confidential data into several edit fields before posting the data to a database. Meanwhile, across the Internet, a wily hacker sits with his Java debugger patiently viewing the contents of these edit fields' values.

The designers of the Java Debugger API (and in turn, jdb) foresaw this security problem and included mechanisms in the API to prevent security problems. Communication between the debugger and the runtime interpreter occurs using a socket-based, proprietary protocol.

Note
The communication protocol between the debugger and interpreter is neither public nor modifiable. Because it uses sockets for communication, however, developers will need a TCP/IP connection in order to use the debugger to debug Java code. Keep this in mind when developing on machines with no network connection.

When the Java Language Runtime is started in -debug mode, the interpreter prints out a password to be used by the debugger. At this time, the runtime also begins to monitor a dynamically chosen port for communications from the debugger. On the debugger side, a correct hostname and password must be specified to connect to the interpreter. Programmers also should be aware that only one debugger instance can connect to an interpreter at a time.

Debugging with jdb

jdb is similar in operation and functionality to the UNIX dbx-style debugger (another Sun tool, by the way). The debugger is used to debug a currently running application. jdb can be used in two different ways for debugging Java applications.

Using the Interpreter Through the Command Line

At its simplest, jdb essentially works through the Java interpreter and loads the Java class indicated on the command line. This can be done by using the following command (exactly like invoking the Java interpreter):

% jdb classname <arguments>

Listing 15.1 uses the jdb debugger to load a copy of the Java interpreter. The class specified on the command line is then run using the specified arguments.

This example creates a simple Java application that displays a screen with the text on it. This text depends on what the user gives as an argument. If no argument is given, then the text "Give argument!" is printed on the screen. The Button is added so that the application will be able to exit gracefully.


Listing 15.1. Using jdb to debug a simple Java application.
import java.awt.*;

/* The following class prints the text "Hello World!" to the screen */
class HelloWorldApp
{
  public static void main(String args[])
  {
    ExitButton button;
    String     lbl;

    button = new ExitButton("Exit!");
    if (args.length == 0)
      lbl = "Give argument!";
    else
      lbl = args[0];

    Frame mainFrame = new Frame("HelloWorldApp");
    Label HelloWorldlbl = new Label(lbl, Label.CENTER);
    mainFrame.add("Center", HelloWorldlbl);
    mainFrame.add("South", button);
    mainFrame.resize(450, 450);
    mainFrame.show();
  }
}

class ExitButton extends Button
{

  public ExitButton(String buttonLbl)
  {
    setLabel(buttonLbl);
  }

  public boolean action(Event evt, Object arg)
  {
    System.exit(0);
    return true;
  }
}

Figure 15.1 shows the screen output with the following input:

Figure 15.1 : Application displaying the command-line argument.

c:\> java HelloWorldApp Howdy!

Figure 15.2 shows the screen output with no arguments given.

Figure 15.2 : Application display with no command-line argument.

Now that this application is apparently running, this is as good a time as any to try out the debugger for the first time.

Note
To examine local (stack) variables, the class must have been compiled using the -g option (javac -g classname).

Enter the following to begin:

% jdb HelloWorldApp Howdy!

The following output (or something similar) should appear on the debugger screen:

% Initializing jdb...
0x139fdf0:class(HelloWorldApp)

If this does appear, the debugger and interpreter have been initialized and are awaiting commands. The entire list of commands will be studied later in this chapter. For now, test out jdb's breakpoint capabilities by entering the following to set a breakpoint:

% stop in HelloWorldApp.main

The debugger should have responded with this:

Breakpoint set in HelloWorldApp.main

The next step should be to actually run the application. The run command is used to do this:

% run

Here is the debugger output:

run HelloWorldApp Howdy!
Breakpoint hit: running ...
HelloWorldApp.main (HelloWorldApp:11)

At this time, the breakpoint already has been hit. The actual syntax used here means that the breakpoint was hit in the HelloWorldApp.main method (or, alternatively, in the HelloWorldApp class, Line 11). Finally, to make sure that the command-line argument was processed properly, do a quick check of the args variable.

% print args

The value should be printed to the screen correctly if all went according to plan.

args = { Howdy! }

The final step in this debugging process is to send the application on its way using the cont command.

% cont

Attaching to a Running Interpreter

The second method available for debugging with jdb is to attach it to an interpreter that is currently running. To do this, that interpreter must have been started using the -debug option. At startup time, this interpreter should have generated a password to be used by jdb. To attach to a currently running interpreter, use the following syntax:

% jdb - host <hostname> -password <password>

To debug an application using this method when the application resides on your local machine, it is necessary to create two separate debug windows. This example repeats the steps necessary to re-create the example in the previous section. However, here we use a Java interpreter that is already running for the debugging connection.

This example reuses the HelloWorldApp class created in Listing 15.1. However, this time the application will be run using the Java interpreter and a separate jdb process.

The first step is to run the application using the Java interpreter and the -debug option.

% java -debug HelloWorldClass Howdy!

The Java interpreter responded with the following password message:

Agent password=k56pn

This password will be different each time the interpreter is run in debug mode and it is randomly generated internal to the Java interpreter. Before continuing, notice that the HelloWorldApp frame window is already showing. This is the drawback to using this method. When the debugger is actually started, the application is already running, so it is difficult to check startup initialization information. However, to track events (such as the button-click), this method still allows the powerful capability of remote debugging!

At this time, in another window, run the jdb debugger using the -password argument (and the -host <hostname> argument if you are currently debugging a remote application):

% jdb -password k56pn

Now that the debugger has been started and has initialized its link with the Java interpreter, you are free to examine all classes currently loaded into memory. For instructional purposes, type in the classes command at the jdb prompt.

% classes
.
.
.
0x13994a8:class(java.awt.Rectangle)
0x13994c0:class(java.awt.Insets)
0x13994d8:class(java.awt.Font)
0x1399548:class(java.awt.Color)
0x13995c8:class(sun.awt.win32.MLabelPeer)
0x13995e8:class(sun.awt.win32.MButtonPeer)
0x1399608:interface(java.awt.peer.ContainerPeer)
0x1399618:interface(java.awt.peer.FramePeer)
0x1399628:interface(java.awt.peer.WindowPeer)
0x1399638:class(sun.awt.win32.Win32FontMetrics)
0x1399648:class(java.awt.FontMetrics)
0x1399680:class(java.awt.Dimension)
0x13996a0:class(sun.awt.ScreenUpdater)
0x13996d8:class(sun.awt.ScreenUpdaterEntry)
0x13996f0:class(sun.awt.win32.Win32Graphics)
0x1399700:class(java.awt.Graphics)
0x13998b0:class(java.io.DataInputStream)
0x13998d0:class(java.net.SocketInputStream)
0x13998f8:class(sun.tools.debug.ResponseStream)
0x1399910:class(java.net.SocketOutputStream)
0x1399938:class(java.io.DataOutputStream)
0x13999a8:class(sun.tools.debug.AgentOutputStream)
0x1399a70:class(java.util.HashtableEnumerator)
0x1399a98:class(java.util.VectorEnumerator)

The preceding listing represents a small portion of the total amount of class information generated. Notice the extremely large number of classes that are listed. These are all of the classes currently in use by either the Java runtime environment, debugger, or the HelloWorldApp application. To explore the environment, type help and a list of all commands available will be shown. This same list (with descriptions of each command) appears later in this chapter.

Debugging Java Applets

The information presented so far dealt with using jdb in combination with the Java interpreter to debug Java applications. However, Java applets run within a container application such as appletviewer. As mentioned in Chapter 11, "Using the Applet Viewer," the applet viewer tool can be run in -debug mode.

This example proceeds through the steps required to properly debug a Java applet. The Java applet created takes in user-entered text and retrieves the URL entered. If this applet is being run inside a Web browser such as Netscape Navigator, the URL will be retrieved and shown. Figure 15.3 shows this applet actually being run in Netscape.

Figure 15.3 : The GetURL applet running in Netscape Navigator.

The source code used to build the GetURL applet is shown in Listing 15.2.


Listing 15.2. GetURL source code (Example3.html).
import java.awt.*;
import java.net.URL;
import java.net.MalformedURLException;
import java.applet.Applet;

/* The following class will load a URL using the URL entered by the user */
/* It also accepts a default URL as input and enters the text
 into the TextField */

public class GetURL extends java.applet.Applet
{
  String       tempString;
  TextField    URLText;
  GetURLButton URLbutton;

  public void init()
  {
    /* First, retrieve the default URL from the HTML file */
    tempString = getParameter("DEFAULT_URL");

    /* Now set up the applet's appearance                 */
    setLayout(new BorderLayout());
    add("North", new Label("Enter a URL to visit:"));
    URLText = new TextField();
    URLText.setText(tempString);
    add("Center", URLText);
    URLbutton = new GetURLButton(this, "Retrieve URL");
    add("South", URLbutton);
  }

  public void GetURLDocument()
  {
    /* Use the MalformedURLException to catch incorrect entries */
    try
    {
      URL tempURL = new URL(URLText.getText());
      getAppletContext().showDocument(tempURL);
    }
    catch(MalformedURLException e)
    {
      URLText.setText("Bad URL!!");
    }
  }
}

//This button will trigger the retrieval of the URL
class GetURLButton extends Button
{
  private GetURL appHandle;

  public GetURLButton(GetURL app, String label)
  {
    appHandle = app;
    setLabel(label);
  }

  public boolean action(Event evt, Object arg)
  {
    appHandle.GetURLDocument();
    return true;
  }
}

Proceed through the following steps to use the applet viewer to debug this applet:

  1. Compile the applet with the -g option so that the debugger can be used to examine local variables. Once again, this is done by invoking the following command: %javac -g GetURL.java.
  2. Run the file Example3.html in a Web browser to test out its capabilities. By default, simply clicking the button will retrieve the JavaSoft home page.
  3. Prepare to debug the applet in the applet viewer. Enter the following command to start the applet viewer in debug mode: %appletviewer -debug Example3.html.
  4. Once jdb is initialized, type run to start the applet viewer with Example3.html.
  5. Enter classes to see a list of all of the currently loaded classes. The class GetURL should appear somewhere in that list.
  6. Notice that the prompt changed from ">" to "main[1]". This means that the main thread (thread #1) is currently selected. Enter the threads command and look for one that designates the GetURL class with the following text: Group group applet-GetURL.class. Select this thread by entering: thread # (where # is the number of the GetURL thread). Now that this has been selected, enter print GetURL to reassure yourself that the GetURL class was loaded and is accessible.
  7. Now dump the contents of the GetURL class. Remember that each object in Java can be printed (or dumped). Its contents should look something like this:
    GetURL = 0x13a5338:class(GetURL) {
        superclass = 0x13a5370:class(java.applet.Applet)
        loader = (sun.applet.AppletClassLoader)0x13a51a0
        static final LayoutManager panelLayout = (java.awt.FlowLayout)0x13a4898 }
  8. To view the actual contents of the thread that is running this class, dump that thread using the "dump t@#" syntax (where # is the thread number). Your output might appear like the following:
    t@4 = (java.lang.Thread)0x13a4a70 {
        private char name[] = "thread applet-GetURL.class"
        private int priority = 6
        private Thread threadQ = null
        private int PrivateInfo = 7217156
        private int eetop = 85851928
        private boolean single_step = false
        private boolean daemon = false
        private boolean stillborn = false
        private Runnable target = (sun.applet.AppletViewerPanel)0x13a48c8
        private boolean interruptRequested = false
        private ThreadGroup group = (sun.applet.AppletThreadGroup)0x13a4a88
    }

As mentioned earlier, jdb is a command-line tool that accepts a number of options. The following section details these options and their meanings.

jdb Options

The jdb debugger enables the developer to perform a variety of options while the Java class is being run. These options range from printing the class's contents to stepping through the class one line at a time. The following table explains each option briefly:

Option NamePurpose
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|method] 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

Other Debuggers

jdb provides the Java programmer with a rudimentary tool that you can use to examine threads, classes, and events. For some developers, all of the capability they could ever want is provided by the jdb tool. However, for many other programmers accustomed to graphical debugging tools such as those found in many C++ environments, Delphi, and Visual Basic, this tool is severely lacking. Because of Java's surge in popularity, tools for Java are on the way that will compare favorably with the best tools of any language. This section will examine common features of modern debuggers with the assumption that Java programmers will soon have these at their disposal. These features also can be used to evaluate new tools as they are released.

Visual Breakpoints

Nearly all GUI development environments currently allow programmers to set breakpoints using some graphical construct (a popular method is to highlight the breakpoint line in red or some other color). This allows the developer to actually see where breakpoints have been set instead of having to store these locations in their own memory. Another convenient feature is the ability to set breakpoints and have the development environment remember where these breakpoints are between sessions. As you have seen, jdb only allows the programmer to set a breakpoint while the program is being debugged. The next time the application is run, these breakpoints will need to be reset. The Symantec Visual Café toolkit includes a debugger that supports this option.

Step Into or Over

Once the breakpoint has been set, most debuggers will allow the programmer to then execute code line by line. If the source code is available for a function call, some debuggers will actually allow the developer to step "down" into the function, all the while monitoring program variables and conditions that may be critical to fixing a problem.

Evaluate and Modify at Runtime

Like jdb, nearly all debuggers allow the developer some mechanism for examining program variables and states while the application is executing. This can be done (like jdb) using print or dump commands, or it may be done using GUI tools.

One exciting feature of many newer environments such as Borland Delphi (perhaps Latte?) and the new Asymetric Java/C++ development tools is the ability to actually modify code and variables at runtime without stopping the application to recompile. This can save huge amounts of wasted development time, particularly in situations where the programmer knows that something is going to crash but would like to step past that point. By modifying program values, you can avoid the known crash in order to explore the unknown bug lurking around the corner.

View Call Stack

Debuggers that allow the call stack to be viewed also provide an extremely useful service, particularly in event-driven programming environments. Many times, methods can be triggered by several sources. At times such as this, it is extremely helpful to be able to see which method called your method. Knowing this can help you to track down otherwise untraceable method calls.

Adding Watches

The use of watches has become extremely popular since the advent of GUI debuggers. When an application is being debugged, development environments that support watches will allow the developer to open a Watch window off to the side. Within this window, any number of objects or properties can be added. As the program is stepped through, the debugger continually updates these values so that the developer can see at all times what is actually happening among several objects or variables.

All of these tools are considered absolute "must-haves" by most professional software developers today. Obviously, jdb falls short in some of these areas. However, it is important to remember that jdb is implemented using the Java Debugger API. Many third-party Java debuggers will be implemented using this same API, so any skills and terminologies learned by using jdb will not be wasted. Instead, these newer tools will simply empower developers to do more with less manual effort.

Summary

jdb is an extremely useful tool, particularly for beginning Java developers. Just as the UNIX dbx debugger remains popular today (many developers use nothing else), there will probably always be a command-line debugger like jdb supplied with the JDK. In many environments where GUI systems are not available (but the Java Virtual Machine is), jdb may be the only option available.

At the current time, no matter what the platform, jdb is probably the most widely used Java debugger. However, as time goes by and more advanced GUI tools are provided, its usage will wane somewhat. However, for beginning software developers, it is imperative that the concepts of debugging introduced here with jdb be understood. Weaker developers may feel that debugging is a necessary evil and can be avoided (by printing to the screen, and so on). The best software developers, however, know that excellent code simply cannot be developed without it.