|
Mastering J2EE Application Development Series
Step 10: Mastering the Art of Debugging
Conquering the Breakpoint
by Satadip Dutta
Learn programmatic mechanisms for debugging Java applications and how to use stack trace information to uncover the root cause of bugs.
As software architects and developers, we do our best to deliver 100% bug-free applications. Nonetheless, more often than not, bugs slip through and end up in released applications. That's why although debuggingthe sometimes onerous task of finding and removing bugsdoesn't stop when the software ships or is deployed, but often continues after the software is released. The most time consuming part of the debugging process is performing root cause analysisfinding the cause of the bug so you can effectively solve the problem and minimize the number of bugs that ship with your code.
One of the keys to successful debugging is obtaining the information from the application code itself, by building in logging mechanisms, for
| "The most time consuming part of the debugging process is performing root cause analysis."
|
example, that you can turn on and off as needed, to facilitate debugging a problem. Applications must be designed with such a goal from the ground up, with architects and developers agreeing about coding conventions for logging, exception-handling, and other information-capture and fault-prevention mechanisms. (Note that some techniques are useful during the development process, but are not appropriate for production code. For example, you can use assertions, new as of J2SE 1.4, but now enabled by default in J2SE 5.0, to check pre- and post-conditions in your code, but assertions are not appropriate for production code.)
A consistent exception-handling strategy can help ensure that your applications generate meaningful stack traces (which we'll get to in a minute). This article introduces several programmatic mechanisms you can use to debug Java applications. We'll look specifically at some of the new features of J2SE 5.0, including some new methods in the Java API, and the new Monitoring and Management API that let's you tap into JMX MBean agent of the JVM, all of which can help you to get to the root causes of bugs. The article then explores using Oracle JDeveloper 10g to debug applications using some of these mechanisms. Let's start with an overview of some of the basics.
The Debugging Process
Finding the root cause of bugs is inherently difficult because any given bug may have numerous potential causes. The first step, then, in debugging is to reduce complexity and understand what is going on inside the application, by capturing the program state at various points. Understanding the program state at a high level can help reduce the scope of the problem. The second step is to identify the precise section in the code that is causing the bug. Logging and Java stack traces are two mechanisms that can be effectively used together to capture and analyze program state.
Logging to capture state information in files: Developers often add println() statements to their code, to generate information to the standard output or standard error while the application is running, for monitoring the application's state at runtime. Sometimes, well-placed println() statements can also help during the debugging process. Although this approach is easy to code, it is not suitable for applications deployed in production environments, since the console's output is transient.
Writing such information to a log file is a better approach, since you'll be able to retrieve the file later for analysis. In general, logs can be used to gain better understanding of program state. For example, you can log the entry to and exit from methods within your code and then look over the logs later, to better understand what's going on in your application.
The Java logging framework (java.util.logging), and other logging utilities, such as the Apache organization's Log4J, provide developers with easy and configurable mechanism to write logging information to a file (see Listing 1 for a sample log created using java.util.logging). Using the Java logging framework, you can easily change the logging level in the associated properties file, so that during the development and debugging process, the log level is set for fine-grain capture, but when the application is deployed, the level is set to severe, so that only real problems are logged.
Listing 1: Sample Log created using java.util.logging
However, even during the development phase, you'll want to exercise some restraint in the logging levelssince too much information can also interfere with the debugging process: if the log files are too verbose, you'll have a hard time getting through them. Tools such as chainsaw can help you filter through a stack of log files, but they don't entirely obviate the need for the judicious use of log levels. In short, be sure to identify not only what should be logged, but also define appropriate logging levels.
Generating Java Stack traces: Java stack traces are universally used by developers to detect and resolve problems in a Java application. A Java stack trace is a read-out of all the threads and monitors in a Java Virtual Machine (JVM) at a particular point in time. We all know what this looks likewhenever your program has a runtime error that you didn't catch in your code, a stack trace is unceremoniously dumped to the console. The stack trace provides information about all pending method calls at a particular point in the execution of the program, tracing back to the statement that threw the exception.
In addition, prior to the release of J2SE 5.0, you could choose to generate a stack trace by sending a signal to the JVMsending a "kill" process in Unix, or using <Ctrl><Break> in Windows to basically quit the JVM in mid-stream; and by throwing and catching exceptions. Neither of these approaches (as means of obtaining meaningful information to facilitate debugging) is particularly useful, and if the application doesn't have a console, or isn't running as a service, they can't help at all. And generating stack traces by using throws and catches for exceptions must be built into all the pre-requisite classes of the application from the beginning, at design time.
| "With J2SE 5.0, developers have new mechanisms for obtaining stack trace information not only more conveniently, but remotely as well." |
With this basic understanding of logging and stack trace generation, let's look at some new features of J2SE 5.0 that you can add to your debugging toolset.
New Java API Enable Both Static and Dynamic Stack Trace Generation
With J2SE 5.0, developers have some new mechanisms for obtaining stack trace information not only more conveniently, but remotely as well, specifically, new methods in the Java API (in the Thread class), as well as new API that let you tap into the JMX infrastructure. Let's look at these more closely.
New API in J2SE 5.0 for Generating Stack Traces: J2SE 5.0 has two new methods in the Thread class that can help you surface the vital information from stack traces without need of the Java console. These new direct hooks into the API to generate stack traces are:
- Thread.getAllStackTraces() which returns a Map of all live threads in the application. (Map is the interface to StackTraceElement objects, which contain file name, line number, class, and method name of the executing line of code).
- Thread.getStackTrace() which returns the stack trace for one thread in an application
Both getAllStackTraces() and getStackTrace() let you save the stack trace data to a log, so you won't need a console. For example, in the example shown in Listing 2, a console is required to actually view the generated stack trace.
To send the stack trace output to a log file (rather than the console), simply set a different location in your coderather than to System.out. For example, you might generate a stack trace for each LDAP request, or for every severe log message. Using the API to log the stack trace can be especially effective for obtaining contextual information about logic problems as they occur.
Listing 2: A test class that gathers all the daemon threads running on the JVM and shows their stack traces
import java.util.*;
public class StackTest {
public void whereami() {
Map <Thread, StackTraceElement[]> st = Thread.getAllStackTraces();
for (Map.Entry <Thread, StackTraceElement[]> e: st.entrySet()) {
StackTraceElement[] el = e.getValue();
Thread t= e.getKey();
System.out.println("\"" + t.getName() + "\"" + " " +
(t.isDaemon()?"daemon":"") + " prio=" + t.getPriority() +
" Thread id=" + t.getId() + " " + t.getState());
for (StackTraceElement line: el) {
System.out.println("\t"+line);
}
System.out.println("");
}
}
public static void main (String args[] ) {
StackTest t1 = new StackTest();
t1.whereami();
}
}
Compile and run the program, and you should see some output like this:
"Finalizer" daemon prio=8 Thread id=3 WAITING
java.lang.Object.wait(Native Method)
java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:116)
java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:132)
java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:159)
"Reference Handler" daemon prio=10 Thread id=2 WAITING
java.lang.Object.wait(Native Method)
java.lang.Object.wait(Object.java:474)
java.lang.ref.Reference$ReferenceHandler.run(Reference.java:116)
"main" prio=5 Thread id=1 RUNNABLE
java.lang.Thread.dumpThreads(Native Method)
java.lang.Thread.getAllStackTraces(Thread.java:1434)
StackTest.whereami(StackTest.java:7)
StackTest.main(StackTest.java:30)
"Signal Dispatcher" daemon prio=10 Thread id=4 RUNNABLE
 |
| Figure 1: Compiling and running the StackTest class in Oracle JDeveloper 10g
|
(Prior to J2SE 5.0, the Throwable class has providedand still doesmethods such as getStackTrace() and printStackTrace(), which are also useful for debugging. For example, getStackTrace() gives you an array of StackTraceElement objects.)
These new methods (Thread.getAllStackTraces(), Thread.getStackTrace()) in the Java API provide static mechanisms to get stack traces from a Java application. If the debugger allows, you can also generate stack traces in conjunction with breakpoints to facilitate root cause analysis. (You'll see how to do this later in this article using Oracle JDeveloper 10g.)
However, although a program may run, when it doesn't behave as expected, you will want to obtain stack traces dynamically, without going back to your source code and embedding in calls to generate logs and so forth. That's where the new Monitoring and Management APIs of J2SE 5.0 come into play.
Using JMX with the new Monitoring and Management APIs in J2SE 5.0 Sometimes it is necessary to generate a stack trace without disrupting the application. Although you can generate stack traces from outside the JVM by using RMIcreate a remote interface, register the stack trace generator with the rmi registry, and then call it using a RMI clientyou don't have to go to the trouble with J2SE 5.0: You can obtain stack traces from the JVM dynamically and a lot more conveniently by taking advantage of the new built-in JMX MBean agent in the JVM from a client.
J2SE 5.0 provides the new management and monitoring APIs that use JMX as the underlying mechanism to expose the JVM information. The use of JMX allows the information to be available locally and remotely to applications that support JMX. It provides a set of pre-defined management interfaces that include ThreadMXBean MBean (from the java.lang.management, new in J2SE5) and provides a management interface for the Thread subsystem of the JVM.
Even though JMX is built-in to the JVM, the JVM must be explicitly enabled for external control. An example of enabling the JVM's built-in management agent (without authentication) is as follows:
java -Dcom.sun.management.jmxremote.port=5001
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false StackTest
Figure 2 shows these settings as configured in the Oracle JDeveloper 10g (Preview Release)
 |
| Figure 2: Enabling the MBean server (management agent) in the JVM
|
We can connect to this server by using a JMX console, or by accessing the JMX MBeans through a proxy. (In the example shown in Listing 2, we use the same port and are connecting to it from the same machine using localhost; the connection is proxied using RMI.)
Listing 2: A simple client that calls the JMX MBean agent (on the JVM) to generate a stack trace map dynamically
import java.lang.management.*;
import javax.management.*;
import javax.management.remote.*;
import java.util.*;
public class JMXClient {
public static void main(String args[]) {
ThreadMXBean t=null;
try {
JMXConnector connector = JMXConnectorFactory.connect( new JMXServiceURL("rmi", "", 0, "/jndi/rmi://localhost:5001/jmxrmi"));
t=java.lang.management.ManagementFactory.newPlatformMXBeanProxy(
connector.getMBeanServerConnection(),
java.lang.management.ManagementFactory.THREAD_MXBEAN_NAME,
ThreadMXBean.class);
}catch (Exception e){System.out.println(e);}
long threads[] =t.getAllThreadIds();
ThreadInfo[] tinfo = t.getThreadInfo(threads,5);
for (ThreadInfo e : tinfo) {
StackTraceElement[] el= e.getStackTrace();
System.out.println("\"" + e.getThreadName() + "\"" + " " +
" Thread id = " + e.getThreadId() + " " + e.getThreadState());
}
The ThreadInfo class contains detailed information about a thread, including ThreadId (getThreadId()), ThreadName, and ThreadState, among other details, and provides the getStackTrace() method to return the stack trace.
You can also get additional information about the various threads in an application (such as deadlocked threads) to facilitate root cause analysis.
| "You can conquer many of the time consuming aspects of debugging by using logs to narrow the scope of your search within the application to just the areas with the potential for bug."
|
In addition to thread related information, you can also obtain information about JVM memory consumption, which can provide insight into the state of the JVM as the program is running.
JMX can also be used to control the logging levels of an application. This mechanism can be very helpful in generating targeted and concise logs that help in understanding the program state and setting breakpoint during trace debugging quickly.
Using Oracle JDeveloper 10g
With that brief overview of some key programmatic mechanisms that enable better understanding of program state to facilitate chasing (and finding) bugs, let's now look at how to use the debugging capabilities of Oracle JDeveloper in conjunction with log and stack trace information, to shorten the time it takes to find errors in your program logic.
A good debugger can facilitate trace debuggingthe process of stepping through code, line-by-line, while closely observing the state of the programwhich can help you get to the root cause of the problem. The JDK includes a simple, no-frills debugger (jdb, which is different in J2SE 5.0 than it was in J2SE 1.4, so be sure to review the documentation if you want to use it) that lets you set breakpoints and step-into and step through your code.
Using the information obtained from stack traces, you can set breakpoints in the appropriate areas of your code, for specific kinds of exceptions: you set a breakpoint for a particular exception and then step through your code to find out where in the code the exception occurs.
Runtime exceptions can often cause abnormal behavior in the logic that can be difficult to track. The stack trace API can be used to record the state of the program by logging the stack trace. Having the stack trace information available in a log file makes it possible to look at the context where the exception occurs. This information is very valuable when using Oracle JDeveloper because it helps you locate the appropriate place in your code to set a breakpoint.
 |
| Figure 3: Oracle JDeveloper 10g (Developer Preview)Setting a breakpoint
|
Oracle JDeveloper allows the setting of various types of breakpoints, such as exception breakpoints, method breakpoints, and class breakpoints. In this example, we will set the method breakpoint.
 |
| Figure 4: Setting a breakpoint on a method
|
When setting the method breakpoint it is also possible to get a stack dump. Setting the check box in the action tab (see Figure 5) can help generate the stack dump. When the method is entered the stack dump is sent to the message window (see Figure 6).
 |
| Figure 5: Generating a stack trace at a specific breakpoint
|
 |
| Figure 6: Setting a breakpoint in the code and observing the output
|
Oracle JDeveloper also provides mechanism to create exception breakpoints. Exception breakpoints allow a developer to specify various exception types, such as an InterruptedException (see Figure 7). The breakpoint occurs when the specified exception is thrown. When using exception breakpoints, it is possible specify a group of breakpoints that allows the breakpoints to be enabled and disabled as a group.
 |
| Figure 7: Setting a breakpoint on a specific exception type
|
Conclusion
You can conquer many of the time consuming aspects of debugging by using logs to narrow the scope of your search within the application to just the areas with the potential for bugs. If you design your applications with debugging in mind from the start, you will have relevant stack trace data saved in log files. Using stack traces to identify possible problem areas can also help you figure out where to set breakpoints in the code. Oracle JDeveloper debugging features allows developers to use the information from the stack traces to find the root cause of the abnormal behavior in a program.
Regardless of the specific tool or technique, a debugger should let you easily set not only source code breakpoints, but also runtime breakpoints, so that you can observe method entry and exit, class loading, and runtime exceptions. Support for features such as these is important because bugs manifest for a variety of reasons, not all of which are immediately apparent. The problem may arise from bad data in method arguments, uncoordinated networking code, or any number of other issues. Also, since most complex applications are distributed in nature, a debugging tool must be able to support remote debugging. For multi-threaded applications, a debugger should enable you to not only watch threads and monitors, but also to detect deadlocked threads.
Next Steps
Debugging next steps
1) Download Oracle JDeveloper 10g (10.1.3) Developer Preview
2) Learn how to debug a multithreaded java application, using the convenient conditional breakpoints
3) Track down memory leaks Using JDeveloper's Debugger
Additional Information:
|
Satadip Dutta (sdutta@vt.edu) is a Software Architect at Hewlett-Packard who has been programming in Java since 1997. He has worked in the areas of web services, distributed resource management, management enablement technologies such as JMX, WBEM, SNMP and DMI, Integrated Development Environments, and user interface design. He is a committer for the XMLBeans project and regularly writes for various technical publications. Satadip holds a Masters Degree in Computer Science from Virginia Tech.
[Back to J2EE Series Home Page]
|