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:
- An object-oriented interface similar to that of the Java language
- Java language runtime features such as threads and monitors
- Support for remote debugging (see the "Security Precautions" section later in this chapter)
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 -debugmode, 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!";elselbl = 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;}}
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 runcommand 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.mainmethod (or, alternatively, in the HelloWorldAppclass, Line 11). Finally, to make sure that the command-line argument was processed properly, do a quick check of the argsvariable.
% 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 contcommand.
% 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 -debugoption. 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 HelloWorldAppclass 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 helpand 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 "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.
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 URLclass 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:
- Compile the applet with the -goption 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.
- Run the file Example3.htmlin a Web browser to test out its capabilities. By default, simply clicking the button will retrieve the JavaSoft home page.
- 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.
- Once jdb is initialized, type runto start the applet viewer with Example3.html.
- Enter classesto see a list of all of the currently loaded classes. The class GetURL should appear somewhere in that list.
- 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 GetURLclass 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 GetURLclass was loaded and is accessible.
- Now dump the contents of the GetURLclass. 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)0x13a51a0static final LayoutManager panelLayout = (java.awt.FlowLayout)0x13a4898 }
- 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 = 6private Thread threadQ = nullprivate int PrivateInfo = 7217156private int eetop = 85851928private boolean single_step = falseprivate boolean daemon = falseprivate boolean stillborn = falseprivate Runnable target = (sun.applet.AppletViewerPanel)0x13a48c8private boolean interruptRequested = falseprivate 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 Name | Purpose |
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 printor 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.
Hi, Great.. Tutorial is just awesome..It is really helpful for a newbie like me.. I am a regular follower of your blog. Really very informative post you shared here. Kindly keep blogging. If anyone wants to become a Java developer learn from Java Training in Chennai.
ReplyDelete