In this chapter, you learn about the Java Debugger API. The current Sun Java Development Kit (the JDK) includes the Java package sun.tools.debug, which provides a simple interface into the Java Virtual Machine. This API allows another program, probably a debugger, to connect and communicate with the Java Virtual Machine to get low-level information about a currently executing Java application. For security reasons, the Java Virtual Machine must be executed in a special debug mode; otherwise, connections are refused. Further, an applet running within a browser does not have access to this API. This API is geared toward development tools.
In this chapter, you learn what the Debugging API is and how to use it. Several examples are included to help demonstrate the API. This information is geared toward an advanced developer who is writing tools or who just wants to learn more about Sun's Java implementation. The Debugging API is within the Java package sun.tools.debug, which is not part of the standard Java classes. Therefore, you may not see it on all implementations of Java Virtual Machines. Java Virtual Machine implementers can define their own Debugging APIs, or none at all. I, however, suspect that any new commercial implementations would support this API, perhaps with small differences or additions.
The Debugger API is built around the concept of remote debugging. This concept implies that not only is the debugger running in a separate process than the debuggee, but it also may be running on a separate machine. This setup offers great flexibility. Besides the obvious benefits of being able to debug from a distance, other benefits do exist. The Java application may be running on a resource-challenged machine such as a PDA, a Set-top-device, or even a toaster. This remote machine may have small amounts of memory, slow CPUs, or small screens, among other things-definitely not a worthy machine for a developer to use for debugging purposes. With remote debugging, the developer can stay within his or her normal development environment on a multi-thousand dollar workstation, while debugging a Java application on a $300 Internet terminal, or even that toaster. Remote debugging is the way to go.
Remote debugging is not very sophisticated; it simply breaks the debugger into several parts. There is the debugger client, the debugger server, and a communication protocol. The debugger server resides in the target-usually code inserted into the target process or perhaps embedded in system software. The debugger server performs the important low-level work of the debugger. The basic functionality of the debugger server is to control the debugger and obtain information on its internal state. The debugger client is the part of the debugger that the developer will interact with. It may provide a fancy user interface and may present a more complicated set of features to the developer. Naturally, all of the features it provides must be implemented using the basic core functionality of the debugger server (for example, if the debugger server provides the ability to set breakpoints but not a single-step command). The debugger client may provide a single-step command by repeatedly setting and removing breakpoints as the user selects the single-step command. Finally, the debugger client and server must communicate, performed via a communication protocol over some transport mechanism. For example, a socket connection may be used and the debugger client will invoke commands on the debugger server by sending it simple messages. Figure 26.1 shows the basic structure of remote debugging.
Figure 26.1 : Remote debugging.
A Java Debugger that uses the Debugging API fits the preceding description of a remote debugger. The debugger itself is written in Java and communicates with the target Java Virtual Machine using the provided classes in the sun.tools.debug package-the Debugger API. The communication between the debugger and the target application running in a remote Java Virtual Machine occurs over sockets. This socket communication, however, is virtually transparent to the user (the user does provide the TCP machine name where the target machine is running). Even debugging on the same machine still utilizes sockets.
The protocol is simple debugger command IDs followed by data specific for the command. All the interaction with the target Java Virtual Machine by the debugger happens by the Debugger calling methods in the supplied classes. The debugger really has no idea the actual work is going on in a separate process. The target Java Virtual Machine reads the commands from the socket, acts on them, and supplies the results back to the debugger. It also can contact the debugger via a callback mechanism. This way, the Java Virtual Machine can notify the debugger when certain events occur.
The Java Debugger API works by setting up agents behind the scenes. The debugger does not have to worry about such details. The agents consist of some nonpublic classes and some threads that are started on both the debugger side and the Java Virtual Machine side. From this point on, I refer to the debugger as the debugger client and the target Java Virtual Machine side of the debugger as the debugger server.
When the Java Virtual Machine is started in debug mode-by supplying the -debug switch when running the Java Virtual Machine directly or when a debugger launches the Java Virtual Machine-a couple of things occur. An extra thread is spawned; it runs a nonpublic class called sun.tools.debug.Agent. This class implements the Runnable interface and runs in a thread named "Debugger Agent." The Agent class handles the communication with the debugger client through the socket and also performs much of the execution of the debugger's commands. The class itself handles a great deal of work, and it also obtains inside information from the Java Virtual Machine via a set of native methods that are implemented in the shared library named agent (libagent.so on Solaris; agent.dll on Win32).
The Agent class also makes
use of several of the other nonpublic debugging classes (see Table
26.1), most notably the BreakpointHandler
class. The BreakpointHandler
class also executes within another thread named Breakpoint Handler.
This thread is contacted when actual breakpoints occur; thus being
in its own thread allows it to contact the Agent thread in an
asynchronous manner. The Agent
class can then pass the information back to the debugger client.
A third, less-important thread also exists. The EmptyApp
class contains a single static main
method (a simple Java program), which is executed as a placeholder
until the real target application is started. It simply lives
in a suspended state.
Agent |
AgentOutputStream |
BreakpointHandler |
BreakpointQueue |
BreakpointSet |
EmptyApp |
LineNumber |
ResponseStream |
Thus, the Java Virtual Machine uses a couple of threads to manage
the communication and execution of debugger commands with the
debugger client. All the debugger knowledge is imbedded within
these classes. They know how to look at the Java Virtual Machine's
internals and how to control the Java Virtual Machine. The debugger
client must simply know how to ask the right questions.
Note |
The debugger server classes are not described in any detail in this chapter. To use the Debugging API you neither need them nor need access to them. |
The debugger client is the program with which the user interacts; it drives the target Java Virtual Machine. The presence of the Java Debugging API makes the task of controlling and getting information from the target Java Virtual Machine trivial. Just about all the details of remote debugging are hidden from the debugger client. The Java Debugger API consists of a handful of classes and a single interface. Each class is discussed later in this chapter, but here is an introduction to what goes on.
The Debugger API performs a number of tasks on behalf of the debugger client. It manages the communication to and from the debugger server. This communication occurs over two socket connections made to the debugger server. One socket is used for sending client requests to the server. The other socket is used for receiving notification events from the server. The requests are synchronous actions initiated by the client-such as the client asking the server for information about the debuggee, or asking the server to perform tasks such as setting a breakpoint. The notification events are asynchronous to the client. That is, the client does not know when they will come, and the notification events may actually arrive while the client is performing requests. Again, the Debugger API hides these details from the debugger client.
Requests are simple to perform. The debugger client invokes the methods defined in the public classes of the Debugger API (see the Remote* classes discussed in the following section). The Debugger API then translates these method calls into command messages and sends them to the server over one of the sockets. The debugger client simply blocks on a method call while this occurs. The debugger server then fulfills the request and simply acknowledges it, or sends information back to the client in a reply message over the same socket. The Debugger API converts the reply into an appropriate return value for the debugger client. All of this communication is handled by the RemoteAgent class. The RemoteAgent class is non-public and is never directly accessed by the debugger client. You should recall the debugger server is using the Agent class to perform the actual work. Think of the RemoteAgent class as a proxy for the Agent class.
Notification events are implemented with a callback mechanism.
The debugger client implements the DebuggerCallback
interface (described in the next section) and registers the callback
with the Debugger API. Once this registration is complete, the
debugger client does not need to perform any other actions. The
methods defined by this interface are invoked, almost magically,
when a notification event occurs-truly a simple process. The Debugger
API-during intialization-creates a thread. This thread is named
"Agent Input" and its only task is to read messages
from one of the sockets-the notification event socket. When a
message arrives from the debugger server, that message is interpreted
and the appropriate method of DebuggerCallback
is invoked. You can think of this as the debugger server calling
the debugger client's code, with the Debugger API hiding the communication
details.
Note |
You must remember that the methods defined by the DebuggerCallback are actually being run by the "Agent Input" thread. As a result, you may need to worry about synchronization issues. |
The communication between the debugger client and server over
the two sockets occurs via a simple message protocol. The messages
are composed of a simple command ID followed by optional data
specific to the command. These command IDs are defined in the
AgentConstants interface.
This interface is not public, but you'll notice some of the public
classes do imple-
ment it.
The Debugger API is initialized by the debugger client when the
client instantiates the RemoteDebugger
class. When this class is intantiated the client passes the callback
object to the Debugger API. At this time the two sockets and the
"Agent Input" thread are created. The call-back object
is any object created by the debugger client which implements
the DebuggerCallback interface.
Note |
Several of the Debugger API classes implement the AgentConstants interface. These classes submit commands to the debugger server. |
To try to make all this information fit together, look at Figure 26.2. As you can see, two socket connections run from the debugger client to the debugger server-one is for request commands, and the other for notification events. The debugger client is concerned only with the RemoteDebugger, DebuggerCallback, and the miscellaneous Remote* classes, which are discussed next. The debugger client controls and queries the target Java Virtual Machine via commands sent down the command socket. It should be prepared to receive requests at any time via the notification socket, via calls to its callback object.
Figure 26.2 : Remote Java Debugger.
Now that you have an idea of what's going on within the Java Debugger API, you can concentrate on the interface that your debugger (or other tool) will use. This interface is via the sun.tools.debug.Remote* set of classes and the single DebuggerCallback interface previously discussed (see Figure 26.3).
Figure 26.3 : Java debugger client classes.
In the following sections, you learn about each of these classes. Many of the classes are similar, and after you have looked at a few and understand how to use them, you will quickly know how to use the rest. For this reason, I do not list every method within each class. This information is readily available via the online documentation that comes with Sun's JDK. I just point out what the class is used for and any possible quirks or special properties that may be of importance. You can actually see many of these classes in action with the included sample applications; they are introduced in the following sections but can be best understood by viewing the source code included on the CD-ROM accompanying this book.
DebuggerCallback is the only
public interface in the sun.tools.debug
package. It simply is the debugger server's gateway into your
code. This class has only five methods: printToConsole(),
breakpointEvent(), exceptionEvent(),
threadDeathEvent(), and quitEvent().
They are all straightforward, but here is a quick overview.
Note |
When you implement these methods, you should understand that they can occur any time and will execute within the context of the Debugger API initiated "Agent Input" thread, not within your thread. |
The function printToConsole()
acts as the debugger server's standard output. In fact, if the
target application writes to its standard output (System.out),
this output is redirected to the debugger client via a call to
DebuggerCallback.printToConsole().
Caution |
Output to System.err is suppressed and does not show up anywhere while a process is being debugged. |
The methods breakpointEvent(), threadDeathEvent(), and exceptionEvent() are each passed an instance of RemoteThread (discussed later in this chapter) from which context information about the event can be obtained. The last of these three methods is also passed a String parameter, which contains an associated message including a stack trace. For an example implementation of this interface, you can view the demo programs below.
The RemoteDebugger class is the primary class that every debugging client program must instantiate. When you create an instance of this object, you are effectively initializing the Debugger API. You cause the debugger client to connect to the target Java Virtual Machine, you register your DebuggerCallback object with the Debugger API, and you allow the Debugger API to perform its background duties, such as starting the agent input thread. The usefulness of this class does not end there.
You use the instance of this class to initiate several operations on the target Java Virtual Machine. You can look at the operations that you can perform via the RemoteDebugger instance as the root command set. With only an instance of RemoteDebugger, for example, you can set a breakpoint on a specific method in a specific class with the following code:
void StopIn( String class_name, String meth_name ) throws Exception
{
// from a class name get a representative for that class
RemoteClass rClass = rDebugger.findClass( class_name );
// now find the method within the class
RemoteField rMeth = rClass.getMethod( meth_name );
// now we can set a break point at the start of the method
rClass.setBreakpointMethod( rMeth );
System.out.println( "Breakpoint set in "+class_name+"."+meth_name );
}
Naturally, you may want to do some more error detecting here, but this example illustrates the basics. As you can see, the whole process starts with the RemoteDebugger instance.
The methods within the RemoteDebugger class are straightforward. Some just provide information-such as freeMemory() and totalMemory(), which return the amount of free memory and total memory, respectively, in the target Java Virtual Machine. Others cause an action to occur on the target Java Virtual Machine. The methods itrace() and trace(), for example, place the target Java Virtual Machine in tracing mode (either instruction or method tracing, respectively). These methods do not cause any other action within the debugger; they simply cause the target Java Virtual Machine to call the standard Java methods traceInstructions() or traceMethods() from the java.lang.Runtime class on itself. This process causes the Java Virtual Machine to output the trace information to its standard output.
When you instantiate the RemoteDebugger class, you can choose from two methods. One attaches your debugger to an already-running Java Virtual Machine, and the other launches a Java Virtual Machine and then attaches to it. If you are attaching to an executing Java Virtual Machine, you must pass the TCP/IP machine the name where the target Java Virtual Machine is executing and the password to RemoteDebugger. This password is emitted by the Java Virtual Machine when it is executed with the -debug flag. If you don't specify the -debug flag, the Java Virtual Machine does not allow a debugger to attach because doing so would be a security violation. The password is a hash of the socket port on which the Java Virtual Machine debugger server is listening.
The Java Debugger API handles the details of decoding the password. If you are executing a new Java Virtual Machine, you simply pass parameters to the Java Virtual Machine like those you would pass on the command line (items like -verbose, -verbosegc, and so on). Regardless of which flavor of the constructor you call, you must pass two additional parameters: your instance of the DebuggerCallback interface and a boolean flag indicating the verbosity mode in which you want the debugger. If you pass a verbosity value of true, you get all kinds of tracing information from the Debugger API.
Here are two sample calls to RemoteDebugger. You also can get a small taste of it in the program easydb, which is included on the CD-ROM (as well as all the samples).
RemoteDebugger db; // hold our instance
if ( attach )
db = new RemoteDebugger( "slapshot", "bas9h", mycallback, true );
else
db = new RemoteDebugger( "-verbose", mycallback, true );
The RemoteStackFrame class provides an interface for obtaining information for a current stack frame of a Java method. Within a Java thread several methods are generally active at any given time. I use the term "active" to describe a method that has been entered but has not exited; it may or may not be executing because it may have invoked another method, but it is still active. Consider, for example, the following chunk of code:
void A() { B(); };
void B() { C(); };
void C() { sleep(1000); );
Assuming that the only way into C() is via A(), then while C() is executing (or sleeping), the methods A(), B(), and C() are active. Each active method has a context associated with it; that context describes the specific invocation of the method (a method can be recursive, so each instance has a distinct context). This context is called a stack frame.
I use the word "stack" because you can often view the set of active methods as a stack of contexts. In the preceding example, for instance, A() is called first, so a stack frame is created. Then A() calls B(), so a new stack frame is created and placed on top of A()'s, or is stacked on top of A(). You therefore can view the whole set of current method calls as a stack of frames, or more simply the callstack. In conventional procedural languages, the frames are often placed adjacent to each other, either going up or down in the address space (so that the callstack can grow toward upper address space or toward lower address space). However, this is an implementation detail and does not have to be the case.
The Sun Java implementation actually allocates each Java method's stack frame from heap storage and just maintains a link to each frame (so, logically, it is still a stack). As you can imagine, the callstack is dynamic and changes throughout the life of a program.
The stack frame contains all the information about the current instance of the method call. An instance of the RemoteStackFrame class, therefore, tells you all about some Java method (native methods are not included). When you obtain an instance of RemoteStackFrame, you can query this object for the following items:
The RemoteStackFrame class is used quite frequently by a debugger. As you can imagine, to view the local variables of a certain method, you obtain its associated RemoteStackFrame object and obtain its list of local variables (which includes parameters) by invoking the getLocalVariables() method for that frame. The code for doing this may look like:
RemoteThread thd = MagicallyGetThread();
RemoteStackFrame frame;
thd.suspend(); //must first suspend thread
frame = thd.getCurrentFrame(); //get the tops stackframe
// get and list local variables
RemoteStackValue[] locals = frame.getLocalVariables();
for(int x=0; x<locals.length; x++)
System.out.println( locals[x] );
thd.resume();
A stack variable is a memory location that lives in a specific stack frame-thus method-local variables and parameters. When the stack frame goes away, the stack variable no longer exists. From an active stack frame, you can obtain an instance of RemoteStackVariable for each stack variable in that frame. With this object, you can get the following information on the associated stack variable:
A stack variable is in scope if the current code position is after the declaration point of the variable. For example,
void meth()
{
System.out.println( "Hello" );
int x = 5;
System.out.println( "BYE" );
}
At the printing of "Hello", the variable x is not in scope, but at the point of printing "BYE", the variable x is in scope. For example, the following code would print the name, type, and value of the stack variables in a specific stack frame.
// get and list local variables
RemoteStackValue[] locals = frame.getLocalVariables();
for(int x=0; x<locals.length; x++)
{
System.out.println( "stack var name:"+locals[x].getName()
+" type:"+ locals[x].getValue().typeName()
+" value:"+ locals.[x].getValue().description() );
}
The RemoteField class represents the fields of a class. Two basic kinds of fields are available: data fields and method fields. Each of these types can have other properties such as static, public, and so on. When you get an instance of this class, you can then query the object for the following information about the associated field:
Whether you are examining a stack variable or a class field, when you query that item for a value, you get back an instance of RemoteValue. More specifically, you get back an instance of a subclass of RemoteValue. This set of classes enables you to obtain information about the specific type of value. A useful method in this class is the description() method. This method usually calls the object's toString() method, which for the simple types returns a string representation of the item's value. But for objects (that is RemoteObject), it includes the class name and the object's numeric ID. The following method will simply report the type and current contents of any value passed to it. This method, of course, accepts all the simple types, such as RemoteInteger, as well as the complex types like RemoteObject or RemoteClass.
void PrintValue( RemoteValue val )
{
System.out.println( " type:"+ val.typeName()
+" value:"+ val.description() );
}
Each simple type in Java has an associated subclass of RemoteValue. All the primitive value classes allow for exactly the same kind of information to be retrieved:
The simple types and their associated remote value classes are
as follow:
byte | RemoteByte |
char | RemoteChar |
short | RemoteShort |
int | RemoteInt |
long | RemoteLong |
boolean | RemoteBoolean |
float | RemoteFloat |
double | RemoteDouble |
An object of the class RemoteObject enables you to get access to the associated object's fields and values, as well as the specific class (see "RemoteClass") associated with the object.
A RemoteArray object enables you to get information about the array size and specific information on its elements. You can, for example, call getElement(), which returns a RemoteValue for the element at the passed index.
The RemoteClass class is important to a tool using the Debugger API. This class enables you to do the expected functions of obtaining information specific to the associated class-its name, fields, values of static fields, and so on.
This class's importance, however, goes further. The RemoteClass class contains the methods to set or clear breakpoints as well as to describe how to handle exceptions. As an example, if you wanted to set a breakpoint in every method within a class, this method would do that.
void BreakOnAllMethods( RemoteClass clazz )
{
// get all the methods
RemoteField[] meths = clazz.getMethods();
// set breakpoints on each method
for( int x=0; x<meths.length; x++ )
{
clazz.setBreakpointMethod( meths[x] );
System.out.println( "Breakpoint set in "+ clazz.getName() +"."+
Âmethos[x].getName() );
}
}
If any value in Java is not one of the basic types listed in the preceding section, it must be an Object type. The RemoteObject class and its subclasses represent this type of value. The subclasses of RemoteObject provide more specific detail to that type of object. String's subclasses, for example, have certain specific behaviors that deserve special attention.
Some of the value classes such as RemoteClass and RemoteThead have specific methods needed by the debugger.
The RemoteString class simply provides routines to obtain the value of the associated String object. It is very simple, much like the simple type classes discussed in the preceding section (such as RemoteInteger, and so on).
Another fundamental class for debuggers to use is RemoteThread. An object of the RemoteThread class type provides access to the remote Thread object on the target Java Virtual Machine. Remember, the thread may or may not be alive; its object may just exist. This class also provides more information. From this class, you can obtain the current stack frames on the thread's callstack. Remember that the Java Virtual Machine can have multiple threads running at a certain point in time. Each thread has its own unique callstack.
You obtain a RemoteThread object from a variety of places. You can get it from a RemoteThreadGroup object. The RemoteDebugger class also has a method to return a list of RemoteThread objects. Finally, when your DebuggerCallback object is contacted because of a breakpoint, an exception, or a thread death notification, it is handed the current thread's RemoteThread object.
What can you do with a RemoteThread object? Because RemoteThread is a subclass of RemoteObject, you have all those methods to obtain information about the thread object. RemoteThread offers more. It is the starting point for obtaining all the current information about the current callstack. After you obtain a RemoteStackFrame object, you can perform many operations. The RemoteThread class also provides many shortcuts for accessing the current stack frame. This frame is special because it represents the method in which the thread is currently running. You also can use methods to easily walk up and down the callstack, obtaining whatever stack frame you want. Finally, and perhaps most important for a thread-aware debugger, you can suspend and resume threads, continue from a breakpoint, and perform single-stepping from the current code position. If you write a tool to use the Debugger API, you will most likely use RemoteThread. Several of the included sample programs (see the following section) show the use of RemoteThread in action.
Another descendent of the RemoteObject class is the RemoteThreadGroup. Naturally, this group is associated with a ThreadGroup object on the target Java Virtual Machine. With this class, you can easily obtain a list of RemoteThread objects that are associated with this RemoteThreadGroup, that is, the threads that belong to the target Java Virtual Machine's ThreadGroup.
This section presents some simple demo programs which use the Debugger API. The demo programs are not really debuggers, but simple utilities. They use various parts of the Debugger API, but not all. When you use the Debugger API, you'll notice that once you know how to use one, using the others is quite similar. For example, using RemoteInteger is just like using RemoteFloat-both simply operate on distinct Java types. Therefore, there is really not a need to show every class being used. The demo programs each have several things in common. They, of course, use the RemoteDebugger class and the DebuggerCallback interface. They also each do a certain amount of examination of the target process. This examination is not interactive like a debugger, but it does function much like a debugger. For example, it suspends a thread and gets the thread's stack frame and then examines that stack frame. Debuggers must perform this type of activity. The examples are meant to get you started. After looking through them and perhaps running them, you should have an understanding of how to use the Debugger API.
The name of the easydb program is both accurate and misleading. It is a very easy program; however, it is not much of a debugger. It does, however, serve a purpose. This program does a little bit of what every Java Debugger does: It creates an instance of RemoteDebugger, which creates a connection to a Java Virtual Machine. The easydb program is sophisticated enough to use both variations of the class's constructors. It can attach to a running Java Virtual Machine, or it can launch one. Thus, you can execute easydb in one of the following ways:
easydb -host slapshot -password bas9h
easydb Simple
Simple is the name of a 20-line sophisticated Java application that does nothing.
Here is the part of the program which creates an object of the class RemoteDebugger. When this object is created, the debugger is attached to the target Virtual Machine. The RemoteDebugger constructor either creates a new Virtual Machine, much like you would run one, or it connects to an existing Virtual Machine. Notice that both constructors are passed the this object. In easydb, the DebuggerCallback interface is implemented by the easydb class and thus this is passed as the callback value. Also, a true value is passed as the last parameter. This places the debugger in a "verbose" mode. This means you will receive a lot of information from the target Virtual Machine about its current activities. It is analagous to running a Java Virtual Machine with the -verbose flag.
//
// call RemoteDebugger
// note we are setting the remote debugger in 'verbose' mode
// (that's the last true in the constructor) which means we will
// be getting a lot of information from debugger api itself
//
if ( host == null )
{
// startup a Java VM and then attach to it
db = new RemoteDebugger( "", this, true );
// set the client off and running...pass it all args
db.run( args.length, args );
}
else
{
// attach to an already running Java VM
db = new RemoteDebugger( host, pass, this, true );
}
In real life you would perform more error detection, but the purpose of this code is just to demonstrate how to use the Debugger API. Once you have an instance of RemoteDebugger, you are connected to the target Virtual Machine and can actually start controlling it. At this point, you have also registered your callback object, and therefore the debugger can also contact you. You will notice above that in the case where a Virtual Machine was started by the RemoteDebugger constructor the db.run() method was immediately invoked. This informs the debugger to actually start running the target Virtual Machine because it was started in a "suspended" state. By starting it suspended, you can perform duties such as setting breakpoints prior to the target application running. In the case in which you attach to an existing Virtual Machine, there is no need for running it because it is already going.
Now that easydb is connected,
it performs its simple duties. First it lists all of the non-standard
classes currently loaded in the target Virtual Machine. Remember,
Java performs dynamic loading on demand, so some classes which
your Java program may use may not be currently loaded. To list
the classes is very simple.
Note |
Java performs dynamic loading of classes on demand. This means all of the classes used by a Java application may not be loaded at certain points in time. |
//
// list the known classes...ignore java.* and sun.tools.debug.*
//
RemoteClass[] classes = db.listClasses(); //easy call to get all loaded Âclasses
System.out.println( "--------------------------------" );
for( int x=0; x<classes.length; x++ )
{
// print only the names of the ones we want
String name = classes[x].getName();
if ( !( (name.startsWith( "java." ) ) ||
(name.startsWith( "sun.tools.debug." ) ) ) )
System.out.println("class: "+name );
}
As you can see, you first invoke the listClasses() method on the RemoteDebugger object. This returns an array of RemoteClass objects. Each element of the array corresponds to a single Java class, which is currently loaded in the Java Virtual Machine. Once you have this array, you can examine any of the attributes of a loaded class with the RemoteClass object. In the case here, you simply get the name of the class with the getName() method. With this same RemoteClass object, you can choose to set breakpoints, list the methods of the class, and examine the class variables (the static data fields), among other operations provided by the RemoteClass class.
The last functional duty performed by easydb is to display the current memory usage of the target Virtual Machine. This simply involves the freeMemory() and totalMemory() methods of the RemoteDebugger class.
//
// now get memory info
//
System.out.println( "--------------------------------" );
System.out.println( "Free Memory: "+ db.freeMemory() );
System.out.println( "Total Memory: "+ db.totalMemory() );
System.out.println( "--------------------------------" );
These two methods perform just like the standard java.System.freeMemory() and java.System.totalMemory() methods. In fact, the debugger client sends a command to the debugger server which then simply invokes those methods within the target Virtual Machine and transfers the results back to the debugger client.
Also notice that the easydb class implements the DebuggerCallback interface itself. This way, it can simply pass itself (via the this reference) to the constructor of RemoteDebugger. Every debugger must provide an instance of this interface. The implementation provided by easydb is perhaps one of the simpler ones you will see. For notifications such as breakpoints and exceptions, it simply prints a message and signals the main program to end. Here is easydb's complete implementation of the DebuggerCallback interface:
/**
* This is called by the Debugger Server (i.e. the target Java VM) via
* the proxy classes (that means the informaton will travel over the
* socket connection) and also by the Debugger Client side of the API.
* The target Java VM will also re-direct the debuggee program's standard
* output to this routine.
*/
public void printToConsole(String text) throws Exception
{
System.out.print( text );
}
/** A breakpoint has been hit in the specified thread. */
public void breakpointEvent(RemoteThread t) throws Exception
{
System.out.println( "Breakpoint: "+ t );
synchronized (this) this.notify(); //end the prog
}
/** An exception has occurred. */
public void exceptionEvent(RemoteThread t, String errorText) throws Exception
{
System.out.println( "Exception: "+t );
System.out.println( errorText );
synchronized (this) this.notify(); //end the prog
}
/** A thread has died. */
public void threadDeathEvent(RemoteThread t) throws Exception
{
System.out.println( "ThreadDeath: "+t );
}
/** The client interpreter has exited, either by returning from its
* main thread, or by calling System.exit(). */
public void quitEvent() throws Exception
{
System.out.println( "Target JVM is gone...." );
synchronized (this) this.notify(); //end the prog
}
Try running easydb with some of your existing Java applications, and then tinker around with it, adding more functionality and so on. You will find that using the Java Debugger API is easy.
JMon is a demonstration of other possible uses of the Java Debugger API. This simple utility continually updates its windows with information obtained from the target Java Virtual Machine. This program shows the basics of using the Java Debugger API.
JMon samples the target Java Virtual Machine periodically and obtains the amount of free memory and total memory. It also provides a list of the current threads in the system. The Java Debugger API makes this task simple to accomplish. This section looks at the code which deals with the Debugger API. The code which implements the AWT windows will not be discussed. It is a very simple text window and is included with the source code on the CD-ROM.
The first thing JMon does is get an instance of RemoteDebugger. This is done via the Connect() method, which is implemented by JMon. Connect() behaves very similar to the code used in easydb, but it does a little more error checking. Connect() is not shown here, but you can view it in the source file included on the CD-ROM. The Connect() method is simply passed the set arguments from the watch command line and returns an instance of RemoteDebugger which the program can use.
// connect to Java VM
RemoteDebugger db = Connect( args );
After getting the RemoteDebugger object and before the main processing loop begins, some setup chores are performed. This loop periodically gathers information about the target Virtual Machine's state, writing it into a buffer-a ByteArrayOutputStream. It then sends this buffer to the AWT window for display. Then the program sleeps for a period of time and wakes up and repeats the cycle.
ByteArrayOutputStream bout = new ByteArrayOutputStream( 100 );
PrintStream out = new PrintStream( bout );
while ( keepgoing )
{
// collect memory information and sent if to viewer
out.println( "Free Memory: "+ db.freeMemory() );
out.println( "Total Memory: "+ db.totalMemory() );
view.Information( bout );
bout.reset();
// collect thread info and send to user
ThreadInfo( db, out );
view.Threads( bout );
bout.reset();
// pause before next sample
Thread.sleep( SAMPLE_TIME );
}
Just as in easydb, the memory information is obtained from the RemoteDebugger object. Information about the current threads in the target Virtual Machine is obtained by the ThreadInfo method. This method is passed the RemoteDebugger object and an output stream to which to write its data. The method is really uncomplicated. First, obtain a list of thread groups within the target Virtual Machine, print the thread group name, and list the names of each thread within the thread group. All of this information is written to a buffer which is then handed to the view which is the AWT text window displaying the data. The thread groups are retrieved with the following code:
// lets get a list of threads by thread group
RemoteThreadGroup[] grps = db.listThreadGroups(null);
The RemoteThreadGroup object not only gives the group name, it also provides the mechanism to get information on the threads within the group. Thus to list the threads within a group, the RemoteThreadGroup object is passed to the ListThreads() method, which is implemented by JMon in the following manner.
private void ListThreads( RemoteThreadGroup grp, PrintStream out ) throws Exception
{
RemoteThread[] thds = grp.listThreads( false );
for( int x=0; x<thds.length; x++ )
{
out.println( " "+ thds[x] );
}
}
Once the listThreads() method is called on the group object, you then get an array of RemoteThread objects from which you can get the names of the threads. It's that simple to get this information using the Debugger API. Obtaining other information about a program is equally simple. The next demo program shows other information you can obtain once you have the RemoteThread objects.
The watch tool is another simple example of other possible uses for the Debugger API. It periodically samples the target Java Virtual Machine and obtains the source location of the currently running method in each thread. The idea of this program is to get statistical data about where the program spends its time. This information is stored in a file and that file can be statically analyzed after the program ends.
First, watch obtains an instance of RemoteDebugger using a Connect() method, which is very similar to the one used in the previous JMon demo. Again, the workings of this method can be viewed in the source file on the CD-ROM. There is not much to learn there. It is worth highlighting the fact that every client of the Debugger API must obtain an instance of RemoteDebugger.
As was mentioned, watch periodically samples the running program and determines the currently executing methods in each thread. The system threads are not included in this monitoring. All Java applications start with a "main" thread group where all new threads and thread groups are placed. Therefore this "main" thread group can be viewed as the parent group for the application. The method SetMainThreadGroup() was created to go out and initially get an instance of RemoteThreadGroup which represents this main group. Later during the sampling phase the search for active threads begins at the "main" thread group, ignoring the system threads. The code for this method is shown later, but for now realize it sets the "main" thread group for this sampling phase. Understanding this, you can look at the main processing loop of watch. It simply loops until told to quit performing a sample and then sleeps.
// get the main threadgroup now so we don't have to keep doing it
// this effectively will allow us to ignore the system group since
// we will be starting our thread search one level down in the "thread tree"
SetMainThreadGroup( db );
// simply loop taking samples until told to quit
while ( keepgoing )
{
// collect sample
Sample( db, out );
// pause before next sample
Thread.sleep( SAMPLE_TIME );
}
This loop is very simple. The real work is going on in the method Sample(). This method is passed the instance of RemoteDebugger as well as a previously created filestream for emitting its results to. Now take a look at what Sample() is doing.
/**
* Get a list of the threads running on the remote
* Java VM.
*/
private void Sample( RemoteDebugger db, PrintStream out ) throws Exception
{
RemoteThread thd = null;
// increment our sample counter
samp++;
try
{
// get a list of all thread below the main group; we will
// recurse through the thread tree.
RemoteThread[] thds = main_grp.listThreads( true /*recurse*/ );
// now go through each thread and print the class/method/line no
// for the top most stackframe
for( int x=0; x<thds.length; x++ )
{
thd = thds[x];
// suspend the thread grab the top frame and then resume
// the thread. once we have the frame, we have a "snapshot"
// of the information we need.
thd.suspend();
RemoteStackFrame frame = thd.getCurrentFrame();
thd.resume();
// note some of these could be null if debug symbols are not present
// this can be detected and we could walk up the call chain looking
// for the first frame with debug symbols and use that one.
out.println( samp + ":"
+ thd.getName() + ">" + frame.getRemoteClass().getName()
&n bsp; Â+"."
+ frame.getMethodName()
+ "(): line:"+ frame.getLineNumber()
+" pc:"+ frame.getpc() );
}
}
catch ( Exception ee )
{
no_problems++;
// we will quit if we get 3 problems in a row
if ( no_problems >= 3 )
{
System.err.println( thd.getName()+": problem accessing stackfrmae" );
throw ee;
}
return;
}
// set our problem flag
no_problems = 0;
}
The first thing each sample does is get a list of the current threads in the main thread group. This is done by using the RemoteThreadGroup object for the main thread group. A call to the listThreads() method with a parameter value of true returns an array containing a RemoteThread object for every thread under the main thread group. The true tells the method to recurse through any subgroups, as opposed to just listing the threads within the group.
With this array of RemoteThread objects, you can now find the currently running method for each thread. This is exactly what occurs in the for loop. You simply step through the array, suspend the associated thread, grab its topmost stack frame, and then resume the thread. The stack frame is obtained by calling the getCurrentFrame() method. This method returns an instance of RemoteStackFrame, and its contents contain a snapshot of the information describing that specific method context at the time the thread was suspended. Thus you can resume the thread right away and then examine the contents. The thread is resumed in order to have as little impact on the target application as possible. After this, the watch program simply grabs the class and method name, the line number, and the pc value for that stack frame. This data is written to the file.
Notice that during the Sample() method any exceptions encountered simply cause the current sample to stop and return to the main loop. This is done in this program because the data being gathered represents a statistical measurement of the target application, and I chose to simply lose a sample rather than perform other actions. The main loop simply sleeps and then performs the next sample. If three exceptions turn up, the program is exited. A more refined tool may act differently. One thing to keep in mind is that the watch program should avoid affecting the target application as much as possible. This is why a thread is suspended for as little time as possible and also why not all of these threads are suspended. The consequence is that there is an opportunity for a thread to end after its RemoteThread counterpart is grabbed.
The last item to examine about watch is the SetMainThreadGroup() method.
private void SetMainThreadGroup( RemoteDebugger db ) throws Exception
{
// lets get a list of threads by thread group
RemoteThreadGroup[] grps = db.listThreadGroups( null );
// for each thread group list its threads
for( int x=0; x<grps.length; x++ )
{
main_grp = grps[x];
//
// the main group when under the debugger is <classname>.main
//
if ( main_grp.getName().endsWith( ".main" ) )
return;
}
// some problem here
throw new Exception( "ThreadGroup 'main' not found." );
}
The RemoteDebugger object is used to get an array of all of the thread groups currently in the target Java Virtual Machine. Each element in the array is a RemoteThreadGroup object. The watch program steps through this array looking for the group whose name ends with ".main." In the Sun JDK v1.01 Solaris implementation used to write this demo, the Virtual Machine always creates a main group where the application is started and therefore where all of its threads and thread groups belong (think of the thread/thread group structure as a tree). The name is the application's main class name followed by ".main." The watch program looks for a match and then considers this the main group and stores that RemoteThreadGroup for later use during the sampling phase.
This is pretty much the heart of the watch program. The DebuggerCallback implementation is very similar to that used in easydb, so there is not much new to show. The entire code for watch is included on the CD-ROM.
After reading this chapter, you should be familiar with the debugger model that the current JDK from Sun implements. You have learned about each class from the Java Debugger API that a debugging tool would utilize. Although the samples are not full-fledged debuggers, they do demonstrate how to use the Java Debugger API.
With the Java Debugger API, much of your debugger must concentrate on user-interface issues and not as much on low-level issues. For example, you do not need to know exactly how a breakpoint is set in the bytecode of the Java Virtual Machine. You have the luxury of just calling a simple method in a class. The Java Debugger API handles the low-level details and leaves the fancy user-interface issues to the tool creator. With these pieces, you should now be prepared to create the next great debugger for the Java community.