by Jordan Olin
One of the hurdles for everyone developing in a new language and execution environment such as Java is learning the appropriate techniques and tools that are available for finding problems in the applications being written (such as "bugs"). Besides providing the standard constructs for creating well-designed, object-oriented applications (inheritance, encapsulation, and polymorphism), Java includes new features such as exceptions and multithreading. These features add a new level of complexity to the debugging process.
The goal of this chapter is to give you a thorough understanding of the debugging facilities that are available with the Sun Java Development Kit (JDK) 1.0.2. As an aid to the JDK, this chapter gives you an in-depth look at the Java Virtual Machine (JVM).
In order to get Java to market as quickly as possible, Sun initially chose not to create a development environment to support the creation of Java applications and applets. Instead, Sun provided features and facilities for developers like us to use in order to create these advanced tools. One of these facilities is the sun.tools.debug package, called the Java Debugger (JDB) API. The API consists of a set of classes that allow the creation of custom debugging aids that may interact directly with an application/applet running within a local or remote instance of the JVM.
The package contains one public interface and 20 public classes that work together to allow you to implement a debugger. The debugging interface is modeled after the client/server architecture. The JVM is on the server that hosts the target application, and your debugger is a client that acts as the interface to control the target application. In order to understand the model, each class of the JDB API is discussed within the following five categories:
The way that your debugger interacts with the running application is through a series of remote classes that act as proxies to the objects in your application. A proxy acts as an intermediary between your debugger and the host JVM. You might think of a proxy as a celebrity with an agent who acts as the public point of contact. You never communicate directly with the celebrity, just with the agent. The agent's sole responsibility is to send messages to the celebrity and relay responses back to the interested party. This is exactly what classes implemented in the sun.tools.debug package do. The proxy model keeps the classes small and gives a clean interface for interacting with the host JVM.
NOTE: Many methods in the JDB API throw the generic Exception exception. This should not be confused with the exceptions that you may or may not want to catch. The exceptions thrown by the API typically represent hard-error situations that occur within the JVM as it is servicing a debugger client request.
One of the most interesting aspects of the debugging facilities that are built into the JVM is their client-server nature. By using the RemoteDebugger class in conjunction with your implementation of the DebuggerCallback interface, you can literally control all aspects of the JVM as it runs an application/applet on your behalf. The debugger client communicates with the JVM via a private TCP/IP connection using a proprietary and undocumented protocol (for security reasons). This private connection is why the source code for the Debugger API is not available in the JDK.
NOTE: Two more classes can be placed in this category: RemoteClass and RemoteThread. RemoteClass supports breakpoint and exception management, as well as descriptive information on the class structure. RemoteThread supports the control of running threads by manipulating the execution state. As they are subclasses of RemoteObject, you will find them described in detail in the section "Special Types" later in this chapter.
public interface DebuggerCallback { public abstract void printToConsole( String text ) throws Exception; public abstract void breakpointEvent( RemoteThread t ) throws Exception; public abstract void exceptionEvent( RemoteThread t, String errorText) throws Exception; public abstract void threadDeathEvent( RemoteThread t ) throws Exception; public abstract void quitEvent() throws Exception; }
Table 46.1 lists each of the public member methods and what they do.
Name | Description |
printToConsole | Called whenever your target applet sends output to System.out or System.err and when the Debugger Agent in the host VM has messages (especially if you create your RemoteDebugger with the verbose flag set to true). |
breakpointEvent | Called when a breakpoint has been reached in the target application. t is the thread that was running when the breakpoint was reached. |
exceptionEvent | Happens when an exception is thrown in the target application. t is the thread that was running when the exception occurred, and errortext contains the message sent with the exception. |
threadDeathEvent | Signals that thread t has stopped in the target application. |
quitEvent | Informs you that the target application has ended. This can be a result of calling System.exit(), or by returning from the main thread of the application. |
Both run as separate processes and use TCP/IP internally to "talk" to your debugger. Once you have created an instance of RemoteDebugger, you will be in direct control of the target application that you will be debugging. You may then begin to make calls against your RemoteDebugger instance to manipulate your debugging session.
NOTE: Four commands start the JVM using the Sun JDK: java java_g appletviewer appletviewer_g The difference is that the _g versions are compiled with a special debugging code that allows the VM to display special trace messages on the target VM's console output (for example, the messages are not redirected to client.printToConsole). These messages include method and instruction tracing output. The output corresponds to the -tm and -t command line options, respectively.
The public API for the RemoteDebugger class is shown in Listing 46.2.
public class RemoteDebugger { public RemoteDebugger( String host, String password, DebuggerCallback client, boolean verbose ) throws Exception; public RemoteDebugger( String javaArgs, DebuggerCallback client, boolean verbose ) throws Exception; public void addSystemThread() public void addSystemThread(thread t); public void close(); public RemoteObject get( Integer id ); public RemoteClass[] listClasses() throws Exception; public RemoteClass findClass( String name ) throws Exception; public RemoteThreadGroup[] listThreadGroups( RemoteThreadGroup tg ) throws Exception; public void gc( RemoteObject save_list[] ) throws Exception; public void trace( boolean traceOn ) throws Exception; public void itrace( boolean traceOn ) throws Exception; public int totalMemory() throws Exception; public int freeMemory() throws Exception; public RemoteThreadGroup run( int argc, String argv[]) throws Exception; public String[] listBreakpoints() throws Exception; public String[] getExceptionCatchList() throws Exception; public String getSourcePath() throws Exception; public void setSourcePath( String pathList ) throws Exception; }
Table 46.2 lists each of the public member methods and what they do.
Name | Description |
RemoteDebugger (String host, String password, DebuggerCallback client, boolean verbose) | The first constructor is used to connect to an existing remote JVM. The host argument is the DNS name of the target machine running the JVM. password is part of the security mechanism used to debug remote applications securely. client is your object that implements the DebuggerCallback interface described previously. And, when set to true, verbose causes informational messages to be sent to client.printToConsole() from the host's JVM. |
RemoteDebugger (String javaArgs, Debugger Callback client, boolean verbose) | The second constructor is very similar to the first, except that it is used to debug Java applications locally. This is probably fine for most GUI applications, but a console application is difficult to debug in this way because the target's output becomes interspersed with the debugger's own informational output. The javaArgs argument should contain valid optional arguments to the java command (excluding the target class). The client and verbose arguments work as mentioned previously. |
addSystemThread() | Add the calling thread to the list of threads which are not suspended by the debugger. These threads are usually Threads used by the debugger itself. |
addSystemThread (Thread t) | Add the specific thread t to the list of threads which the debugger will not suspend. |
close() | Closes down the remote target application/applet and the host JVM. |
get(Integer id) | Returns a proxy for the object identified by id. The returned RemoteObject instance may be cast to its appropriate type. Use ClassName or instanceof to test its type. |
ListClasses() | Returns an array of RemoteClass instances that are resident in the host JVM. |
FindClasses(String name) | Searches the host JVM for a class called name. If the class is not in the VM's class cache, then it is searched for on the target machine. If it is not found, a null is returned. Partial names may be passed in, but there may be ambiguities between user and system classes with the same name. |
ListThreadGroups (RemoteThreadGroup tg) | Returns an array of RemoteThreadGroup instances for the thread groups that are contained in tg. If tg is null, then all thread groups are returned. |
gc(RemoteObject save_list[]) | Causes the garbage collector to be run on the host's JVM in order to free all objects that were requested by this debugger instance. Any objects that were sent to the RemoteDebugger are not garbage collected until this call is made, or the debugger exits. save_list is used to prevent any specific objects that are still being examined by this debugger instance from being collected. |
trace(boolean traceOn) | Toggles the state of the method trace flag in the remote JVM. This command is only valid if you created your RemoteDebugger instance using the constructor that takes javaArgs as the first argument, or if the remote debugging host is one of the _g variants. |
itrace(boolean traceOn) | Toggles the state of the instruction trace flag in the remote JVM. This command is only valid if you created your RemoteDebugger instance using the constructor that takes javaArgs as the first argument, or if the remote debugging host is one of the _g variants. |
TotalMemory() | Returns the total amount of memory available for use by the host JVM. |
FreeMemory() | Returns the amount of memory currently available for use by the host JVM. |
run(int argc,String argv[]) | Causes the host JVM to load in and run a Java class. argv is an array of Strings that represent the command line to execute. The class name must be the first array element. argc is the number of elements that are valid. The RemoteThreadGroup instance that the class is running in is returned on success, otherwise null is returned. |
ListBreakpoints() | Returns the list of breakpoints that are currently active in the host JVM. The format of the breakpoint list is either class.method_name or class:line_number. |
GetExceptionCatchList() | Returns an array of names of the exceptions that the host JVM will send to the debugger as if they are breakpoints. |
GetSourcePath() | Returns the path string that the host JVM uses when searching for the source files associated with a given class. The format is system-dependent. |
setSourcePath(String pathList) | Sets the path string that the host JVM uses when searching for the source files associated with a given class. |
NOTE: When you start up either java, java_g, appletviewer, or appletviewer_g with the -debug flag, a special password is displayed. This value must be used for the password argument.
NOTE: Remember, the output of the method trace and the instruction trace is displayed on the console of the host VM.
The classes in this category represent proxies that give you access to runtime
instance data within the host JVM containing your target. These classes are considered
special because they are used to represent information on data and control elements
other than native types. These proxies allow you to inspect and interact with objects
and classes that are loaded in the host JVM. For example, RemoteObject and
its subclasses represent objects that your target applet has instantiated.
RemoteValue RemoteValue is an abstract class that sits at the root
of the tree of classes which act as proxies to the remote JVM. This class essentially
contains an interface implemented by the classes that follow the interface down the
tree. Because the class contains abstract methods, you never explicitly instantiate
it; rather, you can assume that any of the subclasses consistently and safely implement
RemoteValue's methods. The public API for the RemoteDebugger class
is shown in Listing 46.3.
public class RemoteValue implements sun.tools.java.AgentConstants { // An undocumented // interface containing static // constants used internally // by RemoteValue public String description(); public static int fromHex( String hexStr ); public final int getType(); public final boolean isObject(); public final boolean isString(); public static String toHex( int n ); public abstract String typeName() throws Exception; }
Table 46.3 lists each of the public member methods and what they do.
Name | Description |
description() | Returns a literal description for this instance of the RemoteValue. |
fromHex(String hexStr) | Converts the number in hexStr from its hexadecimal representation to an integer value. |
getType() | Returns the internal numeric identifier for this RemoteValue's type. This value is primarily used internally by the proxy. |
isString() | Returns true if the RemoteValue is a string. |
isObject() | Returns true if the RemoteValue instance is an object type versus a native language type (for example, boolean). |
toHex(int n) | Converts the integer value to its hexadecimal representation in String form. |
typeName() | Returns the literal type name associated with this instance of RemoteValue. |
NOTE: See ShowTypeCodes.java on the CD-ROM for a very simple utility that will display these values.
public class RemoteField extends sun.tools.java.Field // An undocumented class containing the // instance variables that hold the // representative values of the // RemoteField implements sun.tools.java.AgentConstants { // An undocumented interface // containing static // constants used internally by // RemoteField public String getModifiers(); public String getName(); public String getType(); public boolean isStatic(); public String toString(); public String getTypedName() }
Table 46.4 lists each of the public member methods and what they do.
Name | Description |
getModifiers() | Returns the access modifiers for this RemoteField in literal form (for example, public or private). |
getName() | The literal field name. |
getType() | The type of this field as a String (such as int, boolean, or java.lang.String). |
isStatic() | Returns true if the field is designated as static (see getModifiers(), discussed previously). |
toString() | Returns the value of this field in String form, as opposed to its native type. |
getTypedName() | Returns a string which describes the field and its type name. |
CAUTION
You should be aware that as you request RemoteObject (or any subclass) instances from the host JVM, that the host JVM will keep these instances in a nongarbage collected area of memory. Your debugger should either (periodically or on command) call the RemoteDebugger.gc() method in order to release any RemoteObject instances in which you are no longer interested.
public class RemoteObject extends RemoteValue { public String description(); public final RemoteClass getClazz(); protected void finalize() throws Exception; public RemoteField getField( int slotNum ) throws Exception; public RemoteField getField( String name ) throws Exception; public RemoteValue getFieldValue( int slotNum ) throws Exception; public RemoteValue getFieldValue( String name ) throws Exception; public RemoteField[] getFields() throws Exception; public final int getId(); public String toString(); public String typeName() throws Exception; }
Table 46.5 lists each of the public member methods and what they do.
Name | Description |
description | Overrides RemoteValue.description(). |
getClazz() | Returns an instance of RemoteClass that corresponds to this object instance. |
getField() | This overloaded method returns an instance of RemoteField that is based on either a slot number representing the physical position of this field within the object or the literal name of the field. If the field does not exist (slotNum or name are invalid), then Exception is thrown. If name is not found, the RemoteField instance is returned as a null. |
getFieldValue() | This overloaded method returns the value of this field as an instance of RemoteValue. The search is based on either a slot number representing the physical position of this field within the object or the literal name of the field. If the field does not exist (slotNum or name are invalid), then Exception is thrown. If name is not found, the RemoteValue instance is returned as a null. |
getFields() | Returns a list of RemoteField instances representing all of the non-static instance variables defined in this object's class. An exception is thrown if host JVM encounters any problems processing the request. |
getId() | Returns the instance ID that is used uniquely to identify this object in the host JVM. |
toString() | Returns a representation of the object in String form. This is completely dependent on the type of object instance being used. |
typeName() | Overrides RemoteValue.typeName(). |
finalize | Contains code that needs to be executed when this object is collected by the garbage collector. |
public class RemoteClass extends RemoteObject { // Descriptive methods: public String description(); public RemoteObject getClassLoader() throws Exception; public RemoteField getField( int slotNum ) throws Exception; public RemoteField getField( String name ) throws Exception; public RemoteValue getFieldValue( int slotNum ) throws Exception; public RemoteValue getFieldValue( String name ) throws Exception; public RemoteField[] getFields() throws Exception; public RemoteField getInstanceField( int slotNum ) throws Exception; public RemoteField[] getInstanceFields() throws Exception; public RemoteClass[] getInterfaces() throws Exception; public RemoteField getMethod( String name ) throws Exception; public String[] getMethodNames() throws Exception; public RemoteField[] getMethods() throws Exception; public String getName() throws Exception; public RemoteField[] getStaticFields() throws Exception; public RemoteClass getSuperclass() throws Exception; public String getSourceFileName(); public boolean isInterface() throws Exception; public String toString(); public String typeName() throws Exception; // Debugging management methods: public InputStream getSourceFile() throws Exception; public void catchExceptions() throws Exception; public void ignoreExceptions() throws Exception; public String setBreakpointLine( int lineNo ) throws Exception; public String setBreakpointMethod( RemoteField method ) throws Exception; public String clearBreakpoint( int pcLocation ) throws Exception; public String clearBreakpointLine( int lineNo ) throws Exception; public String clearBreakpointMethod( RemoteField method ) throws Exception; public int[] getLineNumbers() throws Exception public int getMethodLineNumber(int index) throws IndexOutOfBoundsException, NoSuchLineNumberException, Exception public int getMethodLineNumber(String name) throws NoSuchMethodException, NoSuchLineNumberException, Exception }
Tables 46.6 and 46.7 list each of the public member methods and what they do.
Name | Description |
description | Overrides RemoteObject.description(). |
getClassLoader() | Returns a RemoteObject instance that represents the Class Loader for this class. |
getField() | This overloaded method returns an instance of RemoteField for a static member based on either a slot number that represents the physical position of this field within the object, or based on the literal name of the field. If the field does not exist (slotNum or name are invalid), then Exception is thrown. If name is not found, the RemoteField instance is returned as a null. |
getFieldValue() | This overloaded method returns the value of a static field as an instance of RemoteValue. The search is based on either a slot number that represents the physical position of this field within the object, or the literal name of the field. If the field does not exist (slotNum or name are invalid), then Exception is thrown. If name is not found, the RemoteValue instance is returned as a null. |
getFields() | Overrides RemoteObject.getFields() but returns an array of RemoteField instances that represent all of the static fields available in this class. |
getInstanceField() | Returns an instance field description as an instance of RemoteField. The search is based on a slot number representing the physical position of this field within the object. If the field does not exist (slotNum invalid), then Exception is thrown. Note that there is no instance data in the field when called in this context. |
getInstanceFields() | Returns an array of RemoteField instances representing all of the instance fields available in this class. Note that there is no instance data in the field when called in this context. |
getInterfaces() | Returns an array of RemoteClass instances representing each of the interfaces implemented by this class. |
getMethod() | Uses name to look up and return, an instance of RemoteField that describes the signature for the specified method. |
getMethodNames() | Returns a String array containing the names of all methods implemented in this class. |
getMethods() | Returns an array of RemoteField instances representing all methods implemented in this class. |
getName() | Returns a String containing the name of this class. |
getSourceFileName() | Returns the file name that contained the Java source statements used to compile this class. This is just a base name and extension (format is OS dependent) without any path information (for example, MyClass.java). |
getStaticFields() | Returns an array of RemoteField instances representing all of the static fields available in this class. |
getSuperclass() | Returns a RemoteClass instance for the superclass of this class. If no superclass was specified (no extends clause was used in the class definition), then an instance of java.lang.Object is returned. |
isInterface() | Returns true if this RemoteClass instance represents an interface versus a class definition. |
toString() | Overrides RemoteObject.toString(). |
typeName() | Overrides RemoteObject.typeName(). |
getLineNumbers | Returns the source file line numbers from the class which have code associated with them, in the form of an array. |
getMethodLineNumber | This method will return the first line of the method which is specified by either a string or the index number. |
Name | Description |
getSourceFile() | Returns an InputStream instance that you may use to display lines from the source file used to create this class (if it is available--a non-null return value). This method is for providing a list type of command in your debugger implementation or for providing interactive source-level debugging. The returned InputStream is typically cast to a DataInputStream prior to use. |
catchExceptions() | Tells the host JVM to return control to your debugger when an instance of this class is thrown. A ClassCastException is thrown if this class is not a subclass of Exception. This makes the exception appear as a breakpoint to your debugger and causes DebuggerCallback.exceptionEvent() to be called. |
ignoreExceptions() | Tells the host JVM not to signal your debugger in the case of this exception being thrown. The host JVM still throws the exception, just not to you. A ClassCastException is thrown if this class is not a subclass of Exception. In practice, it is assumed that catchExceptions() has already been called for this class. |
setBreakpointLine() | Allows your debugger to set a breakpoint based on the source file line number in lineNo. If lineNo is out of range or some other error occurs, a message is returned. Otherwise, an empty string ("") is returned on success. If successful, when your breakpoint is hit, your DebuggerCallback.breakpointEvent() is called. |
setBreakpointMethod() | Allows your debugger to set a breakpoint based on an instance of RemoteField that contains a reference to a method in this class. The breakpoint is placed on the first line of the method. If for some reason method is invalid, an empty string ("") is returned on success. If successful, your DebuggerCallback.breakpointEvent() is called when your breakpoint is hit. |
clearBreakpoint() | Your debugger may remove breakpoints using a valid Program Counter (PC) value as specified in pcLocation. If for some reason pcLocation is invalid, an error message is returned. Otherwise, an empty string ("") is returned. This method has limited value as there is no documented method for specifying a breakpoint in this manor. |
clearBreakpointLine() | Removes a breakpoint that was previously specified for lineNo. If for some reason lineNo is invalid, an error message is returned. Otherwise, an empty string ("") is returned. |
clearBreakpointMethod() | Removes a breakpoint that was previously specified for method. If for some reason method is invalid, an error message is returned. Otherwise, an empty string ("") is returned. |
public class RemoteArray extends RemoteObject { public String arrayTypeName( int type ); public String description(); public final RemoteValue getElement( int index ) throws Exception; public final int getElementType() throws Exception; public final RemoteValue[] getElements() throws Exception; public final RemoteValue[] getElements( int beginIndex, int endIndex ) throws Exception; public final int getSize(); public String toString(); public String typeName(); }
Table 46.8 lists each of the public member methods and what they do.
Name | Description |
arrayTypeName() | Returns a string that contains the type of array elements as a String. The type argument is supplied by calling getElementType(), which is defined later. For any type that is a subclass of java.lang.Object, the string Object is returned. You need to use RemoteValue.typeName() on the RemoteValue instance returned by getElement() or getElements() in order to get the actual object class name. |
description | Overrides RemoteObject.description(). |
getElement() | Returns an instance of RemoteValue containing the value of the array at index. getElement throws an ArrayIndexOutOfBoundsException if index subscripts off the array's boundary. |
getElementType() | Returns a numeric identifier (defined internally) that represents the data type of the elements contained in the array. Use arrayTypeName(), defined previously, to get the literal associated with this type of designator. Note that for any object type. |
getElements() | This overloaded method is used to return an array of either all or a subset of RemoteValue instances. If you specify no arguments, then all values are returned. If you specify beginIndex (zero based) and endIndex (maximum of getSize() - 1), then that specific subset of RemoteValue instances is returned. If either index is invalid, then an ArrayIndexOutOfBoundsException is thrown. |
getSize() | Returns the actual number of elements contained in this array instance. |
toString() | Overrides RemoteObject.toString(). |
typeName() | Overrides RemoteObject.typeName(). |
public class RemoteString extends RemoteObject { public String description(); public String toString(); public String typeName(); }
Table 46.9 lists each of the public member methods and what they do.
Name | Description |
description | Overrides RemoteObject.description() and returns the value in the String object or literal null. |
toString | Overrides RemoteObject.toString() and returns the value in the String object or literal null. |
typeName | Overrides RemoteObject.typeName(). |
The native types classes are all proxies based on RemoteValue that are used to examine any type of field or stack variable based on a nonobject native type. Native types all share an identical interface to allow complete polymorphic use via the RemoteValue abstract type.
The native types supported by this API are described in Table 46.10.
Native Type | RemoteXXX Class |
boolean | RemoteBoolean |
byte | RemoteByte |
char | RemoteChar |
double | RemoteDouble |
float | RemoteFloat |
int | RemoteInt |
long | RemoteLong |
short | RemoteShort |
public class <RemoteXXX> extends RemoteValue { public <native type> get(); public String toString(); public String typeName(); }
Table 46.11 lists each of the public member methods and what they do.
Name | Description |
get | Returns the value contained in the RemoteXXX class as a native value. The <native type> designator as shown in Listing 46.9 may be replaced with any type from the "Native Type" column of Table 46.10 based on the actual RemoteXXX class that the current instance represents. For example, a RemoteBoolean class returns a real boolean value from its get( ) method. |
toString | Overrides RemoteObject.toString(). |
typeName | Overrides RemoteObject.typeName(). |
Once you get to a point in your debugger where you can begin to stop execution
to examine things, then the stack becomes very important. In the JVM, everything
that describes the state of the current method being executed is held in a stack
frame. The stack frame includes items such as the method arguments, local variables,
program counter (PC), method name, and so on. The RemoteStackFrame represents
all of the execution time characteristics of a running Java method and is the proxy
to this unit of control from your debugger. The RemoteStackFrame in turn
provides you with information on data that is physically resident in this stack frame
via RemoteStackVariable instances. These instances are the proxies to the
actual variables that are available for the method currently in context. Currently
in context can be defined as the stack frame method that is active. RemoteStackFrame Table 46.12 lists each of the public member methods and what they do. Table 46.13 lists each of the public member methods and what they do. One of the more challenging tasks when writing Java applications is how to debug
threads. Fortunately for us Java developers, Sun has included special classes to
help debug multi-threaded applications. The thread manager in Java is based on two
constructs: a thread group and the threads themselves. Java uses the thread group
to help categorize and isolate related sets of independent execution paths, or threads. The debugging aids for multithreaded applications allow us to examine and control
the execution of both thread groups and individual threads. This control and execution
is accomplished through the RemoteThreadGroup and RemoteThread
classes. One of the complexities of debugging multithreaded applications is how to manipulate
the individual threads that are active. When you set a breakpoint in a given class's
method, all threads that cross that execution path will break. What actually happens
is that the current thread breaks, and the other threads along the same path suspend.
In this situation, you may use the RemoteThreadGroup and RemoteThread
class to resume the other related threads while you continue to debug (step,
examine, and more) the single thread that you are interested in. Keeping
that in mind, the techniques for debugging a multithreaded application are essentially
identical to debugging a single-threaded application. Debugging multithreaded applications can be difficult depending on the logic of
how the threads share data. The Java language provides many built-in facilities to
allow you to control the concurrency of access that threads have over shared data.
The synchronized keyword, the wait() and notify() methods
of the Object class, and the sleep() and yield() methods
of the Thread class provide features that help you architect your logic
so that sharing data is accomplished safely in a threaded application. Debugging
facilities can help you identify areas of logic that are missing these concurrency
primitives. The following two topics--RemoteThreadGroup and RemoteThread--describe
the proxy classes that allow us to do this manipulation. Table 46.14 lists each of the public member methods and what they do. The public API for the RemoteThread class is in Listing 46.14.
Table 46.15 lists each of the public member methods relating to basic thread control
and what they do.
NOTE: Unlike next(), step() "steps into" any
intermediate method calls that are encountered on the way. For example, if you have
the following lines of source (where PC is the current program counter):
So, by now you are probably asking yourself, "How can I take advantage of
all of this great technology?" First, you can simply use the JDB described in
the next section. Or, you might choose to think of JDB as an example application
and write your own debugger. In this case, the section provides you with some basic
guidelines for using the classes in the sun.tools.debug package. On the CD-ROM included with this book is a file called DebuggerSkeleton.java.
This is a shell for a debugger that is based on the sun.tools.debug package. DebuggerSkeleton.java
shows how to get started by implementing the DebuggerCallback interface
and instantiating an instance of the RemoteDebugger class. You can use the following steps as a guide in implementing a custom debugging
aid with the JDB API:
So, now that you are thoroughly familiar with the JDB API, you are probably wondering
what else it could provide. Think about the debuggers that are available for languages
such as C and C++. These debuggers offer many of the same facilities for debugging,
with one big exception--they actually allow you to modify the target application.
The Sun implementation of a debugging API is really a read-only view into the system. Keeping that in mind, what's on my wish list for this interface? Table 46.19 gives
you an idea. One final point: The information in this section is very specific to the Sun JDK.
Microsoft is in the final stages of implementing the JVM into both the Internet Explorer
3.0 and the next release of Windows OS. There are two major changes:
The JDB API from Microsoft is loosely modeled after the sun.tools.debug package
but has a slightly different naming convention. Also, the JDB API supports some of
the missing features of the Sun package noted previously.
NOTE: Be aware that the following information is close to final but is
not officially released. Implementation details may have changed between now and
the time that Microsoft ships these new features.
Now that you have a good understanding of the underlying debugging facilities
in the JDK, JDB can be examined. JDB really serves two purposes:
Our discussion of JDB covers all of the commands in detail and describes the major
portions of the JDB API as they are used.
As an application, JDB is patterned after the DBX debugger found on many UNIX
systems. This is a command line-oriented debugger that allows you to interact with
a running application by entering English-like commands for examining and controlling
its execution state. These commands allow you to examine variables, set breakpoints,
control threads, and query the host JVM about the classes that it has loaded. You
may also have the host JVM load classes for you in advance so that you can set breakpoints
in methods prior to their execution. In order to understand fully the architecture of JDB, it may be helpful to print
out or have access to its source while you are reading this section. When you install
the JDK, there is a relatively small ZIP file in the installed JDK root directory
(typically, \ JAVA in theWindows versions) called SRC.ZIP. This file contains the
Java source files for all of the publicly documented Java classes, including the
class that is the foundation for JDB. The SRC.ZIP file must be unzipped with the
Use Directory Names option in order to preserve the source tree. The source tree
follows the package-naming convention used in the JDK. So, if you unzip the SRC.ZIP file into a directory under \JAVA called SRC, you
would find two subdirectories under \JAVA\SRC called \JAVA\SRC\JAVA and \JAVA\SRC\SUN.
These represent the Java source files in the java.* and sun.* collection
of packages, respectively. The source to JDB is based on a class called TTY, or sun.tools.ttydebug.TTY.
The source file (assuming the directory structure above) would be in \JAVA\SRC\SUN\TOOLS\
TTYDEBUG\TTY.JAVA. As you look at TTY.java, the first thing you will probably notice is that TTY
is a simple class that derives from Object, as all classes with no extends
clause do. But, it does implement the DebuggerCallback interface as I mentioned
previously in the section "Putting It All Together." You should note that there are a few instance variables to help support application
currency. Specifically, a reference to a RemoteDebugger instance (debugger),
a Remote ThreadGroup (currentThreadGroup), and a RemoteThread
(currentThread) are needed in order to maintain context within the currently
executing application. This helps when implementing most of the commands that query
for information from a suspended thread and method. After a few private methods,
you will see the methods defined in DebuggerCallback, allowing TTY to complete
its contract by implementing this interface. It is now probably easier to jump to the bottom of the source and see its real
structure. It starts with a main method that will be called first when TTY.class
is loaded and run. This main essentially parses, collects, and verifies
all of the command-line arguments and, if everything looks good, creates an instance
of the TTY class. The rest of the processing takes off from TTY's custom
constructor. The only constructor in TTY takes seven arguments; these arguments specify the
following:
Once in the constructor, the remote debugger instance is created and, if specified,
the initial class is loaded. Creating an instance of RemoteDebugger actually
causes a JVM to be started for you in the remote system (even if no starting class
is specified). After that, a check is made for an input command file. Finally, the
command processing loop is started. The command loop's real functionality lies in a method called executeCommand
that expects a tokenized version of the input command line. executeCommand
is simply a series of cascading if-else-if statements that check to see
if the first token of the input argument matches one of the predefined sets of commands
supported by JDB. If so, then the helper method associated with that command is executed.
Otherwise, an appropriate error message is displayed ("Huh? Try help..."). So, now that you have a feel for the general structure of JDB and its source (the
TTY class), look at the actual command-line arguments and commands supported
by JDB.
In order to start JDB in its simplest form, you can type jdb <Enter>
at your command prompt. JDB then performs its initialization and presents you with
a > prompt. In fact, three "formal" command lines start JDB
in various modes, as follows:
JDB [-dbgtrace] [<java-args>]
JDB [-dbgtrace] [<java-args>] <classname>
[<args>]
StackFrame Listing 46.10
public class StackFrame {
public StackFrame();
public String toString();
}
Listing 46.11
public class RemoteStackFrame
extends StackFrame {
public int getLineNumber();
public RemoteStackVariable getLocalVariable( String name ) throws Exception;
public RemoteStackVariable[] getLocalVariables() throws Exception;
public String getMethodName();
public int getPC();
public RemoteClass getRemoteClass();
}
Name
Description
getLineNumber()
Returns the line number relative to the beginning of the source file that is associated
with the current position of this stack frame in a suspended thread.
getLocalVariable()
Returns the RemoteStackVariable instance associated with name in
this RemoteStackFrame instance.
getLocalVariables()
Returns an array of RemoteStackVariable instances that are available in
this RemoteStackFrame instance.
getMethodName()
Returns a String containing the name of the method that is represented by this RemoteStackFrame
instance.
getPC()
Returns the JVM's PC relative to the first bytecode of the method that this RemoteStackFrame
instance represents. The PC is like a pointer into the Java bytecode stream and is
moved as each bytecode is interpreted by the VM.
getRemoteClass()
Returns an instance of RemoteClass that defines the method represented by
this RemoteStackFrame instance.
RemoteStackVariable Listing 46.12
public class RemoteStackVariable
extends LocalVariable { // This is a private class that contains the data
// items that are exposed via Remote-
// StackVariable methods
public String getName();
public RemoteValue getValue();
public boolean inScope();
}
Name
Description
getName
Returns a String that contains the literal name of this RemoteStack-Variable
instance.
getValue
Returns a RemoteValue instance that contains the data value for this variable
at this moment. This object may be cast to an appropriate RemoteValue subclass.
inScope
Returns true if this RemoteStackVariable instance is currently in scope.
A RemoteStackVariable is out of scope if the block that defines it is not
in context: For example, a counter defined within a for loop construct or
an exception variable defined within a catch statement.
Thread Management
RemoteThreadGroup Listing 46.13
public class RemoteThreadGroup
extends RemoteObject {
public String getName() throws Exception;
public RemoteThread[] listThreads( boolean recurse ) throws Exception;
public void stop() throws Exception;
}
Name
Description
getName
Returns the name of the current RemoteThreadGroup instance.
listThreads
Returns an array of RemoteThread instances that exist in the current RemoteThreadGroup
instance. If recurse is set to true, then all embedded RemoteThreadGroups
are traversed and their member RemoteThread instances are returned as well.
stop
Stops the execution of all threads belonging to this thread group. This is very useful
if you are debugging a multithreaded application with many thread instances running
on the same execution path. Alternatively, you could use listThreads(),
and manually choose the threads to stop.
RemoteThread
Listing 46.14
public class RemoteThread
extends RemoteObject {
// Basic Thread Control
public String getName() throws Exception;
public String getStatus() throws Exception;
public boolean isSuspended();
public void resume() throws Exception;
public void stop() throws Exception;
public void suspend() throws Exception;
// Execution path control
public void cont() throws Exception;
public void next() throws Exception;
public void step( boolean skipLine ) throws Exception;
// Stack frame control
public void down( int nFrames ) throws Exception;
public RemoteStackFrame[] dumpStack() throws Exception;
public RemoteStackFrame getCurrentFrame() throws Exception;
public int getCurrentFrameIndex();
public RemoteStackVariable getStackVariable( String name ) throws Exception;
public RemoteStackVariable[] getStackVariables() throws Exception;
public void resetCurrentFrameIndex();
public void setCurrentFrameIndex( int iFrame );
public void up( int nFrames ) throws Exception;
}
Name
Description
getName()
Returns a String containing the name of this RemoteThread instance.
getStatus()
Returns a String containing the literal status of this RemoteThread instance.
isSuspended()
Returns true if this RemoteThread instance is suspended.
resume()
Resumes execution of this RemoteThread instance from the current program
counter. It is assumed that the thread is currently suspended.
stop()
Terminates execution of this RemoteThread instance. You cannot resume execution
after you stop the thread, but you can examine the current stack frame.
suspend()
Suspends execution of this RemoteThread instance at its current location.
This is similar to the thread instance receiving a breakpoint. Once suspended, you
may use Program Execution Control methods to step through the thread and the Stack
Frame Control methods to examine the variables of the current frame. Execution of
the thread may continue upon execution of the resume() method.
Table 46.16 lists each of the public member methods relating to execution path control
and what they do.
Name
Description
cont()
Resumes the current RemoteThread instance from a breakpoint. If the thread
is suspended, use resume() instead of cont().
next()
Executes to the next line of source in the current RemoteThread instance
and does not "step into" any method calls on the way. That is, it executes
any intermediate method calls without giving you the opportunity to stop and examine
variables and more. This method throws an IllegalAccessError exception if
the thread is not suspended or processing a breakpoint. Also, if there is no line
number information for this class, then next operates like the step()
method below.
step( )
Executes either the next instruction or goes to the next line if skipLine
is true. Executing step(false) at this point puts the PC at the first instruction
of evaluateCounter(), where as calling next() executes the call
to evaluate-Counter() and leaves the PC pointing to the first instruction
of line 3.
1: myCounter += 1;
PC 2: evaluateCounter( myCounter );
3: System.out.println( "myCounter: " + myCounter );
Table 46.17 lists each of the public member methods relating to stack frame control
and what they do.
Name
Description
down()
Moves the stack frame that is currently in context for this RemoteThread
instance down nFrames levels. This is typically used after executing the
up() method in order to "walk" back down the call stack frames.
This command, when used in conjunction with up(), may be used to help implement
an interactive call stack window. If this Remote-Thread instance is not
suspended or at a breakpoint, then an IllegalAccessError exception is thrown.
Also, if nFrames is too great (for example, you try to go past the bottom
frame on the execution stack), an ArrayOutOfBounds exception is thrown.
dumpStack()
Returns an array of RemoteStackFrame instances representing the execution
stack up to and including the current stack frame. To display the call stack, you
can iterate through the array of RemoteStackFrame instances and call its
toString() method.
getCurrentFrame()
Returns the RemoteStackFrame instance for the current frame.
getCurrentFrameIndex()
Returns the index to the current RemoteStackFrame in the execution stack.
getStackVariable()
Returns the RemoteStackVariable instance associated with name in
the current stack frame. A null instance is returned if name is
not found.
getStackVariables()
Returns the array of RemoteStackVariable instances that are contained in
the current stack frame. These represent both arguments to the method and local variables
(whether they are in scope at this point).
resetCurrentFrameIndex()
Restores the current stack frame to its state prior to making any calls to up(),
down(), or setCurrentFrameIndex().
setCurrentFrameIndex()
Establishes the stack frame at level iFrame to be the current stack frame
in the host JVM.
up()
Moves the stack frame that is currently in context for this RemoteThread
instance up nFrames levels. This is typically used after a breakpoint in
order to "walk" up the call stack frames. When used in conjunction with
down(), this command may be used to help implement an inter- active call
stack window. If this RemoteThread instance is not suspended or at a breakpoint,
then an IllegalAccessError exception is thrown. Also, if nFrames
is too great (for example, you try to go past the top frame on the execution stack),
an ArrayOutOfBounds exception is thrown.
Putting It All Together
Category
Description
General
These commands handle the general flow of the debugger's control. You can take advantage
of the RemoteDebugger class instance to handle these commands. Consider
options such as context commands (set the current thread group and thread), memory
commands, tracing, and more as potential commands for this category.
Informational
These commands display information on the current debugging target. You use instances
of RemoteObject, RemoteClass, and RemoteStackFrame to
display objects, classes, methods, variables, and source lines.
Breakpoints
These commands are used to set/reset breakpoints and exceptions. These may be implemented
by the methods in RemoteClass and RemoteThread.
Execution
These are the commands that may be used once a breakpoint happens, a thread suspends,
or an exception is thrown. You can again use RemoteClass and RemoteThread
to process these requests.
Whats Missing?
Wish
Description
Modifying Values
This would actually allow you to change the value of fields in objects, arguments
to methods, and local variables.
Sending Input
You can only receive output from the target application. There is no way to send
input to it.
Breakpoints
You do not have the ability to set a breakpoint at a specific program counter location
within a stack frame.
Data Breakpoints
You do not have the ability of setting a breakpoint on any kind of data item. Setting
a breakpoint could be accomplished manually by controlling everything in the context
of a breakpoint handler, but this would be very slow.
Local Trace
The trace output should be able to be redirected to the DebuggerCallback
interface.
Bytecodes
You should be able to get the Java bytecode disassembly off of a RemoteStackFrame
instance.
The previous tables list the areas that allow you to make a complete debugging environment.
I'm sure there are others. In time, hopefully, Sun will help us all out.
What About Microsofts Java Implementation?
Table 46.20 shows the relationship between the Microsoft debugging classes and the
sun.tools.debug package.
sun.tools.debug
Microsoft COM Debug
DebuggerCallback
IRemoteDebugManagerCallback
IRemoteProcessCallback
RemoteDebugger
IRemoteDebugManager
IRemoteProcess
IEnumRemoteProcess
RemoteValue
IRemoteDataField
IRemoteContainerField
RemoteField
IRemoteField
IEnumRemoteField
RemoteOject
IRemoteObject
IEnumRemoteObject
RemoteClass
IRemoteContainerObject
IRemoteClassField
RemoteArray
IRemoteArrayField
RemoteString
IRemoteStringObject
RemoteBoolean
IRemoteBooleanObject
RemoteByte
IRemoteByteObject
RemoteChar
IRemoteCharObject
RemoteDouble
IRemoteDoubleObject
RemoteFloat
IRemoteFloatObject
RemoteInt
IRemoteIntObject
RemoteLong
IRemoteLongObject
RemoteShort
IRemoteShortObject
StackFrame
RemoteStackFrame
IRemoteStackFrame
RemoteStackVariable
IRemoteMethodField
RemoteThreadGroup
IRemoteThreadGroup
IEnumRemoteThreadGroup
RemoteThread
IRemoteThread
IEnumRemoteThreadThe JDB in Depth
Basic Architecture
The JDB Command Line
Option
Meaning
-dbgtrace
If specified, enables verbose messages to be returned from the JVM instance that
is created in order to run the target Java application/applet. These messages are
sent to the printToConsole callback method and have the format [debugger:
<some message>]. That way, you can filter them in your printToConsole
implementation and send them to a log file or window, for example.
<java-args>
This is an optional subset of the arguments that you can specify when running the
java command to start an instance of the JVM. The currently recognized options
are the following: -cs, -checksource, -noasyncgc, -prof,
-v, -verbose,-verify, -noverify, -verifyremote,
-verbosegc, -ms, -mx, -ss, -oss, -D,
and -classpath.
Option
Meaning
-dbgtrace
Same as above.
<java-args>
Same as above.
<classname>
This mandatory argument is the .class file to load initially into the JVM and control
by the debugger. Use the run command to begin execution.
<args>
This argument represents any arguments needed by <classname>.
It must be specified here, as there is no way to set up arguments for <classname>'s
main method after JDB starts up.
JDB [-dbgtrace] [-host <hostname>] -password <password>
Option | Meaning |
-dbgtrace | Same as above. |
-host <hostname> | This optional argument specifies a DNS name or IP address of a computer running the host JVM for the Java application/applet that you will be debugging. If this argument is not specified, then localhost is automatically assumed. |
-password <password> | This mandatory argument is the password that was displayed on the console when the JVM hosting the application/applet to be debugged was loaded. It is generated by either java, java_g, appletviewer, or appletviewer_g when the -debug flag is specified on the respective command lines. |
If you choose to run a debug session repeatedly with a specific set of commands,
you can create a command input file for JDB to use automatically. The command file
is a simple ASCII text file where each line contains a valid JDB command followed
by the line delimiter that is appropriate to the OS you are running JDB on (for example,
CR/LF for Wintel). JDB looks in the following three places, in this order,
for a command input file (see Table 46.21).
Directory (System Property) | File Name | Example |
USER.HOME | JDB.INI | C:\JAVA\JDB.INI |
USER.HOME | JDBRC | /JAVA/.JDBRC |
USER.DIR | STARTUP.JDB | ./STARTUP.JDB |
Now that you know how to start JDB, it is useful to know how to operate it as well. In order to show some of the features of the JDB command set, I use a small, threaded application where each thread increments a shared counter and displays the value. Listing 46.15 contains the application.
public class MTTest extends Thread { static int count = 0; static Object sema = new Object(); public MTTest( ThreadGroup theGroup, String threadName ) { super( theGroup, threadName ); } public void run() { String myName = getName(); while (true) { synchronized (sema) {if (count < 30) { System.out.println( myName + ": " + count ); count += 1; } else break; } yield(); } System.out.println( "Exiting " + getName() ); } public static void main( String[] args ) { ThreadGroup theGroup; // To contain the threads MTTest[] theThreads; // Array of threads // Wait for the user to hit <Enter> to start System.out.print( "MTTest: Press the <Enter> key to begin." ); System.out.flush(); try { System.in.read(); } catch (java.io.IOException e) {} System.out.println(""); // Create the thread group theGroup = new ThreadGroup( "MTThreads" ); // Create the thread array theThreads = new MTTest[3]; // Create and start the threads for (int i = 0; i < theThreads.length ; ++i) { theThreads[i] = new MTTest( theGroup, "T" + Integer.toString(i)); if (theThreads[i] != null) theThreads[i].start(); } } }
One of the first things to know about debugging anything that is multithreaded with JDB is that it is best to have the target application wait until you are ready and run it in a separate process space. Running in a separate process space prevents the target application's output from getting interspersed with the debugger output. Also, having the application wait for you means that it won't start running as soon as you start up the JVM that will run your application.
In order to follow along with the examples associated with each command, complete the following steps:
javac -g MTTest.java
java_g -debug MTTest
Agent password=xxxxxx
where xxxxxx is the password to use when starting up JDB, which is the next step.
jdb -host localhost -password xxxxxx
>stop in MTTest.run
Breakpoint set in MTTest.run
Breakpoint hit: MTTest.run (MTTest:12)
T0[1]
Now that everything is ready to go, the specific commands that are implemented by JDB can be examined. For clarity, I have broken the commands into groupings based on their functionality. By using these categories, you can put each command into its respective slot (see Table 46.22).
General | Context | Information | Breakpoint | Exception | Threads |
help/? | load | classes | stop | catch | suspend |
exit/quit | run | dump | clear | ignore | resume |
memory | threadgroup | list | step | kill | |
gc | thread | locals | next | up | |
itrace | use | methods | cont | down | |
trace | |||||
!! | threadgroups | ||||
threads | |||||
where |
NOTE: kill and next, itrace, and trace are undocumented but implemented commands.
These are the commands that are used to control some of the features of the debugger
or interrogate the state of the remote JVM.
help/?
This command displays a list of the "documented" commands that are supported
by JDB.
exit/quit
Uses: RemoteDebugger.close()
This command terminates your debugging session and JDB. The connection between
JDB and the remote JVM is broken. If debugging locally, the VM is shut down.
memory
Uses: RemoteDebugger.freeMemeory() and RemoteDebugger.totalMemory()
This command displays the total amount of used and free memory in the remote JVM. For example, on my system, the memory command displays the following:
Free: 2674104, total: 3145720\
gc
This command causes the garbage collection task to run on the remote JVM. The
classes that are not in use by the debugger are freed. JDB automatically tells the
JVM not to garbage collect the classes involved with the current RemoteThreadGroup
and RemoteThread instances. If you are having a long debug session, then
you should use the gc command to occasionally remove the RemoteClass
instances that have been cached on your behalf by the remote JVM.
itrace (an Undocumented Command)
Use: RemoteDebugger.itrace()
This command enables (on) or disables (off) bytecode instruction tracing on the remote JVM that is hosting your application. The output is sent to System.out on the remote JVM and cannot be intercepted from within your debugging session.
The following is sample output from an itrace:
1393B58 6B4AD8 ifeq goto 6B4ADD (taken) 1393B58 6B4ADD aload_0 => java.net.SocketInputStream@139EE80/ 1481298 1393B58 6B4ADE aload_1 => byte[][2048] 1393B58 6B4ADF iload_2 => 0 1393B58 6B4AE0 iload_3 => 2048 1393B58 6B4AE1 invokenonvirtual_quick java/net/SocketInputStream.socketRead([BII)I (4)
trace (an Undocumented Command)
Use: RemoteDebugger.trace()
This method enables (on) or disables (off) method call tracing on the remote JVM that is hosting your application. The output is sent to System.out on the remote JVM and cannot be intercepted from within your debugging session.
The following is sample output from a trace:
# Debugger agent [ 3] | | | < java/lang/Runtime.traceMethodCalls(Z)V returning # Debugger agent [ 2] | | < sun/tools/debug/Agent.handle(ILjava/io/DataInputStream; Ljava/io/DataOutputStream;)V returning # Debugger agent [ 2] | | > java/io/DataOutputStream.flush()V (1) entered # Debugger agent [ 3] | | | > java/io/BufferedOutputStream.flush()V (1) entered # Debugger agent [ 4] | | | | > java/net/SocketOutputStream.write([BII)V (4) entered # Debugger agent [ 5] | | | | | > java/net/SocketOutputStream.socketWrite([BII)V (4) entered # Debugger agent [ 5] | | | | | < java/net/SocketOutputStream.socketWrite([BII)V returning # Debugger agent [ 4] | | | | < java/net/SocketOutputStream.write([BII)V returning # Debugger agent [ 4] | | | | > java/io/OutputStream.flush()V (1) entered # Debugger agent [ 4] | | | | < java/io/OutputStream.flush()V returning # Debugger agent [ 3] | | | < java/io/BufferedOutputStream.flush()V returning # Debugger agent [ 2] | | < java/io/DataOutputStream.flush()V returning # Debugger agent [ 2] | | > java/io/FilterInputStream.read()I (1) entered # Debugger agent [ 3] | | | > java/io/BufferedInputStream.read()I (1) entered # Debugger agent [ 4] | | | | > java/io/BufferedInputStream.fill()V (1) entered # Debugger agent [ 5] | | | | | > java/net/SocketInputStream.read([BII)I (4) entered # Debugger agent [ 6] | | | | | | > java/net/SocketInputStream.socketRead([BII)I (4) entered
The format of the call information uses the signature described in the section
on the class file, which is described in a subsequent section on the .class file
structure. These commands are used to establish context for the debugging session. They set
up the state of the remote JVM and the instance variables used operationally by TTY.
In order to use just about any of the commands in JDB, the current thread group and
current thread must be set. The initial context is set automatically when you use
the run command; otherwise, you must manually set it using the threadgroup
and thread commands. load causes the remote JVM to search for and load <classname>.
If you do not fully qualify the name of the class to load, the VM tries
to look in well-known packages to complete the name. If the class is not found, an
error message is returned. Also, an error message is displayed if no <classname>
is provided. This command does not affect the current context. This command loads and begins execution of <classname>
or the last <classname> specified on the previous call to
the run command. Error messages are returned if the class can't be found
or if there is a general failure in attempting to start <classname>.
This command also sets the context by establishing initial values for the currentThreadGroup
and currentThread. This command establishes <thread group name> as the default
thread group by putting a reference to its RemoteThreadGroup instance in
the currentThreadGroup instance variable. This command is required for using
any of the commands that are relating to breakpoints, exception, and thread management.
For example, you could enter: >threadgroup MTThreads
to specify the current default thread group. This command sets <thread id> as the current thread in
context relative to the current thread group by putting a reference to its RemoteThread
instance in the currentThread instance variable. This command is required
for using any of the commands relating to breakpoints, exception, and thread management.
It is typically used in conjunction with the threadgroup command. For example,
you could enter: >thread 5 to specify the current default thread. T0[1] is now the new prompt showing
you that your context is in thread T0, which is the first thread in the
current thread group. This command is used to display or set the path that the remote JVM uses to find
.class and .java files. If called without any arguments, then the current source
file path is displayed. If called with a path (formatted like the classpath system
property), then the source file path is updated accordingly. For example, to display
the current class/source path and then change it: These commands are used to display information about the classes that are currently
loaded and known to the remote JVM. They are list-oriented in nature and depend on
the context being established as described previously. This command displays the class and interface names that are currently known to
the remote JVM hosting the debugging target. If this list is unusually large, try
running the gc command to free instances of RemoteClass that are
being held on your behalf by the remote JVM and the RemoteDebugger agent. The following is sample output from the classes command after starting
up MTTest: dump Syntax: dump t@<thread id> | $s<slot
id> | 0x<class id> | <name>, where t@<thread
id> represents a valid thread ID within the current thread group;
$s<slot id> represents the slot/offset to a variable
in a stack frame; 0x<class id> represents the
numeric identifier for a currently loaded class; or <name>
represents the literal this, a valid class name,
a field name (for example, class.field), an argument name, or a local
variable name. This command dumps the detailed description of the specified thread, stack-based
variable, class, field, named local variable, or named argument. If an argument,
variable or field is requested, its name and value are displayed. If a thread or
class is specified, a detailed description of the thread or class is displayed, including
instance variables and their current values. The following is an example of dumping the MTTest class: Note that the second line is a result of the search algorithm used by the dump
command. This command displays one or more source lines for the current thread's current
method. There must be a thread in context that is running but in a suspended state.
Also, the line number, if specified, must be relative to the top of the source file
that defines the current method. Otherwise, if you don't specify a line number, then
the current line is displayed. This listing includes the four lines of source immediately
before and after the specified line. The following is how a list with no arguments should look for MTTest at the current
breakpoint: The => in line 10 denotes the current line of source. This command displays all arguments to the current method and local variables
that are defined in this stack frame. You must have a thread in context, and you
must have compiled your code with the -g option in order to get symbol table
information for the local variables and arguments available for debugging. For example, if you entered locals, you should see the following: The this argument is present in all methods and is pushed on the stack
implicitly by the JVM invocation logic. The myName variable is not in scope
yet, as you are at a breakpoint at the beginning of the method. This command displays all of the methods in the specified class, including the
signature of each. The methods list for MTTest should look like this: The <init> method is a special name and represents the constructor
for this class. This command displays a simple description of the specified thread, stack-based
variable, class, field, named local variable, or named argument. If an argument,
variable, or field is requested, then its name and value are displayed. If a thread
or class is specified, then the name and ID of the thread or class are displayed. The following is an example of printing the MTTest class: threadgroups This command displays the name and description of all active thread groups in
the remote JVM. The threadgroups command for MTTest looks like this: threads This command displays the list of threads for the current or specified thread
group. If the current or specified thread group has embedded thread groups, their
threads are listed as well. Issuing the threads command for MTTest's named thread group MTThreads
should give you something that is similar to the following: where This command displays the call stack (the list of methods that were called in
order to get to this point) for the current thread (as set with the thread
command), all threads (for the current thread group as set with the threadgroup
command), or the specified thread (by its ID). On my system, the command where all gives the following result: These commands allow you to set/remove and control execution flow from a breakpoint.
Breakpoints are the fodder for most debugging sessions, in that just about
the only way to do anything while debugging is to stop the application. Or, in Java's
case, a thread must be stopped unconditionally at some point. That's exactly what
a breakpoint does. Once you have established a breakpoint (you have already seen
this briefly in step 4 while setting up the debugging session), you execute your
program/thread until the execution path reaches that point. When it does, the execution
of that thread stops, and you regain control of the remote JVM and your application. Now you can set up and remove breakpoints, "walk" through your program
using the step and next commands, or use cont to continue
execution. Syntax 2: stop at <classname>:line number | 0x<class
id>:line number Uses: RemoteDebugger.findClass or RemoteDebugger.get and RemoteClass.setBreakpointLine This command sets a breakpoint at the first bytecode instruction of the specified
method (Syntax 1) or at the first bytecode instruction of the specified line. If
Syntax 2 is used, line number is relative to the beginning of the source file
that contains <classname>/<class id>. If stop
is issued with no arguments, then the existing breakpoints are displayed. When a
breakpoint is placed on a method that is part of a multithreaded application/applet,
it applies to all active threads when they cross that method or line of code. The
breakpoint remains active until it is removed with the clear command. The following example lists the current breakpoints, sets one at line 14 of MTTest,
and then displays the breakpoint list again: clear Syntax 2: clear <classname>:line number | 0x<class
id>:line number Uses: RemoteDebugger.findClass or RemoteDebugger.get, and RemoteClass.clearBreakpointLine This command clears an existing breakpoint at the first bytecode instruction of
the specified method (Syntax 1) or at the first bytecode instruction of the specified
line. If Syntax 2 is used, line number is relative to the beginning of the
source file that contains <classname>/<class id>.
If clear is issued with no arguments, then the existing breakpoints are
displayed. When a breakpoint is cleared from a method that is part of a multithreaded
application/applet, then it affects all active threads when they cross that method
or line of code. The following example lists the current breakpoints, clears one at the start of
MTTest.run, and then displays the breakpoint list again: step This command executes the next instruction of the currently stopped thread. If
the next instruction is a method call, execution stops at the first instruction of
the method being invoked. An error is generated if there is no current thread
or the current thread is not suspended at a breakpoint. Like step, the next command steps execution of the currently
stopped thread to the next instruction. But, if the next instruction is
a method invocation, then the method is called and control returns to the debugger
upon return from the method being executed. At that point, the current instruction
is the one immediately following the call. As with step, an error is generated
if there is no current thread or the current thread is not suspended at a breakpoint.
This command continues the execution of all suspended threads in the default thread
group. The command is useful when you have been single-stepping through a thread
and want to let the application simply run until the next breakpoint or exception
occurs.
These commands control which exception classes should be caught or ignored by
JDB. One of the more interesting aspects of Java is the notion of exceptions. Exceptions
are kind of like intelligent breakpoints that you can code logic for directly within
your application. They are typically used to trap very specific exceptional situations
that should not occur under normal use. A feature of the JDB API is the ability to register your interest in a specific
exception and have it behave like a breakpoint so that you can step through the logic
coded in the catch block of the exception handling code. If you choose not
to handle a breakpoint in this manner, then the debugging client is notified as if
a nonrecoverable breakpoint were reached. That control is returned to the
debugger, but you will not be able to step through the exception logic in the catch
block. This command causes the debugger to catch (via DebuggerCallback.exceptionEvent)
occurrences of the exception class specified by <classname>/0x<class
id> when thrown by the remote JVM. Throwing of the exception
class causes execution to stop, as if a breakpoint were placed at the first executable
statement of the catch block that is currently active when trying the specified
exception. In other words, all try-catch blocks in the application that
"catch" the specific exception will become breakpoints. If no class is
specified, then the existing caught exceptions are displayed. An error is generated
if the specified class is not a subclass of Exception. This command causes the debugger to stop catching occurrences of the exception
class specified by <classname>/0x<class id>
when thrown by the remote JVM. This does not stop the exception from being
thrown, but it does stop the debugger from being able to catch the exception
as if it were a breakpoint. If no class is specified, then the existing caught exceptions
are displayed. An error is generated if the specified class is not a subclass of
Exception.
These commands are used to control the execution state and stack of currently
active threads. The thread control commands are something like a manual breakpoint,
in that you can stop a thread in its tracks without needing a breakpoint to be coded.
This can be extremely useful in situations where you have inadvertently coded an
endless loop and can't see where the flawed logic exists. You can also resume execution
of a thread you have suspended as well as remove it entirely. Once you have suspended
a thread, you can then manipulate the current frame within the call stack that is
active. This allows you to examine arguments and local variables in methods that
called the currently suspended method. This command suspends (stops) the execution of the specified thread(s) or all
nonsystem threads if no thread is specified. This causes a one-time breakpoint to
occur at the currently executing instruction in the affected thread(s). This command resumes (continues) execution of the specified thread(s) or all nonsystem
threads if no thread is specified. This allows the affected thread to run as if no
breakpoints ever existed at the previously suspended code location. This command terminates (permanently stops) the execution of either all threads
in the specified thread group or just the specified thread (by ID). Once a thread
or thread group has been terminated, it may not be resumed, and no breakpoint commands
(step, next, cont) may be applied. An error is generated
if no arguments are specified or if the specified name or ID is bad. This command moves the context of the current stack frame from its current position
up one (the default), or n frames. A frame represents the execution state
for a method that was being executed prior to calling into another method. The execution
state includes the location of the line being executed when the method call was made,
the line's arguments (and their current values), and the line's local variables (and
their current values). At this point, the method's state is "pushed," and
a new frame is created for the method being called. Each time a method is subsequently
called from within the next method, the current frame is "pushed" and a
new frame is put into context. By moving "up" the call stack, the prior
method's arguments and variables may be examined. This command is used after using the up command. down moves
the context of the current stack frame from its current position down one (the default),
or n frames. By moving "down" the call stack, you progress toward
the current state of execution prior to the last suspended point in the current thread
(whether from a breakpoint, caught exception, or explicit call to suspend).
As you can see from the number of commands (33 distinct ones), JDB is actually
a very powerful debugging facility. It is an excellent example of how to implement a debugger using Sun's JDB API.
Furthermore, it is a great sample application for Java system programming. In reality,
it would not take much work to wrap some GUI functionality around the TTY
class to make it a little more visually appealing (subclass it, add a multiline text
field for the output, set up menu items for the command groups, create pop-up windows,
and more). Even if you don't work with the GUI functionality in this manner, you
can use JDB to debug your applications on any platform that is supported by the Sun
JDK. What about debugging strategies? The biggest point to keep in mind is that if
you are running into problems in a multithreaded application, you are going to want
to liberally use breakpoints and exceptions. The nice thing about throwing exceptions
in your own code is that you can catch them like breakpoints without having to know
specific source line numbers or method names and ignore them as well. (Remember,
an exception does not have to mean that a catastrophic error has occurred. It is
simply a signal from one piece of code to another.) You can create an exception to
throw when a loop counter goes beyond a certain value, when a socket receives a particular
message, and so on. The ways that exceptions can be used are truly limitless. Another important feature to use is the where command. You will not realize
you are in a recursive situation unless you see what the call stack looks like. Your
logic may have intentional recursion, but it is not a technique that the average
business application uses on a regular basis. Once you have a feel for the look of
the call stack, you can then use the up and down commands to move
your context through the call stack and check out local variables/arguments that
might be affecting your execution. The last strategy is to have an in-depth understanding of how your compiled class
files are formatted and how the JVM uses them. That's what is covered in the next
two chapters.
!! (Repeat Last Command)
This command re-executes, or repeats, the last entered command. It is not something
that is implemented by any remote class; rather, this command is just a feature that
is enabled by the JDB command processor.
Context Commands
load Syntax: load <classname>
Use: RemoteDebugger.findClass()
run Syntax: run [ <classname> [args] ]
Uses: RemoteClass.getQualifiedName and RemoteDebugger.run()
threadgroup Syntax: threadgroup <thread group name>
Uses: RemoteDebugger.listThreadGroups and RemoteThreadGroup.getName
thread Syntax: thread t@<thread id> | <thread
id>, where <thread id> is an integer
constant representing a thread's ID number. (See the threads command.)
Use: RemoteThreadGroup.listThreads
T0[1]
use Syntax: use [source file path]
Uses: RemoteDebugger.getSourcePath and RemoteDebugger.setSourcePath>use
.;c:\java\lib\classes.zip
>use .;c:\java\lib\classes.zip;c:\java\lib\classdmp.zip
>use
.;c:\java\lib\classes.zip;c:\java\lib\classdmp.zip
Information Commands
classes Syntax: classes
Uses: RemoteDebugger.listClasses and RemoteClass.description0x1393768:class(MTTest)
0x1393778:class(sun.tools.debug.Agent)
0x13937a0:class(java.lang.Runtime)
0x1393818:class(java.net.ServerSocket)
0x1393830:class(java.net.PlainSocketImpl)
0x1393840:class(java.net.SocketImpl)
0x1393890:class(java.net.InetAddress)
Uses: RemoteThreadGroup.listThreads, RemoteThread.getStackVariables,
RemoteStackVariable.getValue, RemoteDebugger.get, and RemoteDebugger.findClassT0[1] dump MTTest // Could also have entered: dump 0x1393768
"MTTest" is not a valid field of (MTTest)0x13a0ca8
MTTest = 0x1393768:class(MTTest) {
superclass = 0x1393008:class(java.lang.Thread)
loader = null
private static Thread activeThreadQ = null
private static int threadInitNumber = 2
public static final int MIN_PRIORITY = 1
public static final int NORM_PRIORITY = 5
public static final int MAX_PRIORITY = 10
static int count = 0
}
T0[1]
list Syntax: list [line number]
Uses: RemoteThread.getCurrentFrame, StackFrame.getRemoteClass,
RemoteClass.getSourceFileName, RemoteClass.getGetSourceFileT0[1] list
6 }
7
8 public void run() {
9
10 => String myName = getName();
11
12 while (true) {
13 synchronized (sema) {
15 if (count < 30) {
T0[1]
locals
Use: RemoteThread.getStackVariablesT0[1] locals
Method arguments:
this = Thread[T0,5,MTThreads]
Local variables:
myName is not in scope.
T0[1]
methods
Uses: RemoteDebugger.get, or RemoteDebugger.findClass and RemoteClass.getMethodsT0[1] methods MTTest
void <init>(ThreadGroup, String)
void run()
void main(String[])
T0[1]
print
Uses: RemoteThreadGroup.listThreads, RemoteThread.getStackVariables,
RemoteStackVariable.getValue, RemoteDebugger.get, and RemoteDebugger.findClassT0[1] print MTTest
MTTest = 0x1393768:class(MTTest)
T0[1]
Uses: RemoteDebugger.listThreadGroups, RemoteThreadGroup.getName,
and RemoteThreadGroup.descriptionT0[1] threadgroups
1. (java.lang.ThreadGroup)0x13930b8 system
2. (java.lang.ThreadGroup)0x139ec60 main
3. (java.lang.ThreadGroup)0x13a0b00 MTThreads
T0[1]
Uses: RemoteDebugger.listThreadGroups, RemoteThreadGroup.getName,
RemoteThreadGroup.listThreads, RemoteThread.getName, RemoteThread.description,
and RemoteThread.getStatusT0[1] threads MTThreads
Group MTThreads:
1. (MTTest)0x13a0b30 T0 at breakpoint
2. (MTTest)0x13a0b90 T1 suspended
3. (MTTest)0x13a0bd0 T2 suspended
T0[1]
Uses: RemoteThreadGroup.listThreads, RemoteThread.dumpStack, and
RemoteStackFrame.toStringT0[1] where all
Finalizer thread:
Thread is not running (no stack).
Debugger agent:
[1] sun.tools.debug.Agent.handle (Agent:590)
[2] sun.tools.debug.Agent.run (Agent:324)
[3] java.lang.Thread.run (Thread:294)
Breakpoint handler:
[1] java.lang.Object.wait (Object:152)
[2] sun.tools.debug.BreakpointQueue.nextEvent (BreakpointQueue:46)
[3] sun.tools.debug.BreakpointHandler.run (BreakpointHandler:184)
main:
[1] MTTest.main (MTTest:51)
T0:
[1] MTTest.run (MTTest:12)
T1:
Thread is not running (no stack).
T2:
Thread is not running (no stack).
T0[1]
Breakpoint Commands
stop
Uses: RemoteDebugger.findClass or RemoteDebugger.get, RemoteClass.getMethod,
and RemoteClass.setBreakpointMethodT0[1] stop
Current breakpoints set:
MTTest:10
T0[1] stop at MTTest:12
Breakpoint set at MTTest:12
T0[1] stop
Current breakpoints set:
MTTest:12
MTTest:10
T0[1]
Uses: RemoteDebugger.findClass or RemoteDebugger.get, RemoteClass.getMethod,
and RemoteClass.clearBreakpointMethodT0[1] clear
Current breakpoints set:
MTTest:12
MTTest:10
T0[1] clear MTTest.run
Breakpoint cleared at MTTest.run
T0[1] clear
Current breakpoints set:
MTTest:12
T0[1]
Use: RemoteThread.step
next (An Undocumented Command)
Use: RemoteThread.next
cont
Uses: RemoteThreadGroup.listThreads, RemoteThread.cont, and RemoteThread.resetCurrentFrameIndexException Commands
catch
Uses: RemoteDebugger.getExceptionCatchList, RemoteDebugger.findClass,
or RemoteDebugger.get and RemoteClass.catchException
ignore
Uses: RemoteDebugger.getExceptionCatchList, RemoteDebugger.findClass
or RemoteDebugger.get, and RemoteClass.ignoreExceptionThread Commands
suspend
Uses: RemoteThreadGroup.listThreads and RemoteThread.suspend
resume
Uses: RemoteThreadGroup.listThreads and RemoteThread.resume
kill (An Undocumented Command)
Uses: RemoteThreadGroup.listThreads, RemoteThread.stop, and RemoteThreadGroup.stop
up
Use: RemoteThread.up
down
Use: RemoteThread.downJDB Wrap-Up