Java Debugging Tools
Java Debugging Tools
Table of Contents
If you're viewing this document online, you can click any of the topics below to link directly to that section.
1. Tutorial tips 2. Introducing debugging in Java applications 3. Overview of the basics 4. Lessons in client-side debugging 5. Lessons in server-side debugging 6. Multithread debugging 7. Jikes overview 8. Case study: Debugging using Jikes 9. Java Debugger (JDB) overview 10. Case study: Debugging using JDB 11. Hints and tips 12. Wrapup 13. Appendix
2 4 6 11 15 18 20 22 28 30 33 34 36
Java debugging
Page 1
ibm.com/developerWorks
Navigation
Navigating through the tutorial is easy: * * Select Next and Previous to move forward and backward through the tutorial. When you're finished with a section, select the next section. You can also use the Main and Section Menus to navigate the tutorial.
Java debugging
Page 2
ibm.com/developerWorks
If you'd like to tell us what you think, or if you have a question for the author about the content of the tutorial, use the Feedback button.
Getting help
For questions about the content of this tutorial, contact the author, Laura Bennett, at [email protected] . Laura Bennett is a Senior Programmer at IBM. She holds a Bachelors Degree in Computer Science from Pace University add a Masters Degree in Computer Science from Columbia University. Laura is developerWorks' evangelist for Java as well as one of the site's architects. In her spare time, she enjoys playing with her Lego MindStorm Robot and building objects with her four-year old's TinkerToys.
Java debugging
Page 3
ibm.com/developerWorks
The versions of JDK, JSDI, JSP, and HTML you are using all have an impact on your choice. IDE and stand-alone GUI debuggers are the easiest for the novice programmer and prove to be the most time efficient. The debuggers will lead you to where your program crashed. Execute your program in the debugger, using a mouse to set breakpoints and step through the source code. The downside of using these debuggers is that not all IDE debuggers support the latest Java APIs and technologies (such as servlets and EJB components). Text-based and the brute-force techniques offer more control but will probably take longer for the less-experienced Java programmer to actually find bugs. We call them the "poor man's" debugger methods. If any of the above still do not meet your needs, the Java platform has introduced the Java Debugging APIs, which you may use to create a debugger that specifically meets your needs.
Java debugging
Page 4
ibm.com/developerWorks
* *
The revised Java Debugger (JDB) serves as both a proof of concept for the Java Debugging API, and as a useful debugging tool. It was rewritten to use the Java Debug Interface (JDI) and is part of the JDK. JDB is discussed in Section 9.
Java debugging
Page 5
ibm.com/developerWorks
Setting breakpoints
The first step in debugging is to find out where your coding went wrong. Setting breakpoints helps you accomplish this. Breakpoints are temporary markers you place in your program to tell the debugger where to stop your program execution. For example, if a particular statement in your program is causing problems, you could set a breakpoint on the line containing the statement, then run your program. Execution stops at the breakpoint before the statement is executed. You can then check the contents of variables, registers, storage and the stack, then step over (or execute) the statement to see how the problem arises.
Java debugging
Page 6
ibm.com/developerWorks
Various debuggers support several types of breakpoints. Some of the most common are: * * * * * * Line breakpoints are triggered before the code at a particular line in a program is executed. Method breakpoints are triggered when a method that has been set as a breakpoint is reached. Counter breakpoints are triggered when a counter assumes or goes beyond a particular value. Exception breakpoints are triggered when code throws a particular type of exception. Storage change breakpoints are triggered when the storage within a particular storage address range is changed. Address breakpoints are triggered when the address a breakpoint is set for has been reached.
Note: Some debuggers only support certain types of breakpoints in compiled versions of Java code (using a just-in-time compiler) and not within interpreted code (code that is compiled by the javac tool). An example is the Address breakpoint. Each tool may be slightly different in the way you can set breakpoints. Check your tool's documentation. You might ask, how do I know where to place the breakpoints? * If you are completely in the fog as to where the problem is, then you can set a breakpoint at the beginning of your program in the main() method. * If your code generates a stack trace, set breakpoints at the area where it died in the stack trace. You will see line numbers for the source code within the stack trace. * If a particular part of your output or graphical display is not presenting information correctly (for example, a text field is displaying the wrong text), you can set a breakpoint where the component is created. Then you can step through your code and display the values that are assigned to the GUI object.
Experience will teach you to set your breakpoints in the most appropriate places. You can set more than one breakpoint within a single class or program. Typically, you will disable or enable breakpoints, or add or delete breakpoints as you are debugging code. Tools should allow you to see where all of your breakpoints are and give you
Java debugging
Page 7
ibm.com/developerWorks
* *
Inspecting variables
Typically a program is core dumping because a value of a variable is not set correctly. The most common scenario is trying to compute or compare operations against variables with null values or dividing by zero. The easiest way to find out if that is the case is by inspecting the values where the error occurs. More likely than not, a variable did not get assigned a value at the point you expected it to. Visual debuggers usually have a monitoring window where they display the values of all the variables local to the current class that you are currently in. Some debuggers even display the address of the variable and may even let you dynamically change the value to see if the program will continue to execute as you originally expected it to. Command-line debuggers typically offer commands to handle the same feature. Using the command line feature, you can even investigate the entire contents of an array by displaying every row and column's contents. While most debuggers only show local variables that are in the scope of the class in a monitoring window, some allow you to continue to monitor a variable after it falls out of scope. Some debuggers support the viewing of registers. Note there are cases where registers can only be viewed in compiled Java programs and not interpreted programs.
Java debugging
Page 8
ibm.com/developerWorks
Stack traces
When a Java program core dumps, it generates what is called a stack trace to the console window. The stack trace tells the developer the exact path the program took to get to the point where the problem occurred. The stack trace will state the class and method name and the source code line number (if you compiled with the debug option). If you start at the beginning of the trace and work down, you can proceed backwards through your code to see what statements were actually executed. This is one way to quickly determine what went wrong in your program. You can manually force the generation of a stack trace using either the following statements. * * Throwable().printStackTrace() to generate a trace of the method's code at a single point in time. The trace will show the method's calls across threads. Thread.currentThread.dumpStack() to generate a snapshot of the current thread only.
You want to force a stack trace when you need to understand under what conditions your program generated its output. An example of forcing a stack trace appears below. This code snippet method creates copies of files. We check if the copy succeeded by comparing the length of both files. If they are not equal, we set up a trace to a file and then force printing of a stack trace (see the statement in bold). Throwable() is a class in the java.lang package. printStackTrace() is a method in the Throwable() class that prints out a trace of your program execution path.
public static boolean copyFile( String sourceFile, String targetFile) { ........ ........ // see if the copy succeeded. if (success) { // see if the correct number of bytes were copied long newFileLength = new File(targetFile).length(); if (oldFileLength != newFileLength)
Java debugging
Page 9
ibm.com/developerWorks
{ Debug.trace(1, sourceFile + Constants.BLANK_STRING + Long.toString(oldFileLength)); Debug.trace(1, targetFile + Constants.BLANK_STRING + Long.toString(newFileLength)); Throwable().printStackTrace(); return false; } } else { Debug.trace(1, sourceFile); Debug.trace(1, targetFile); return false; } ........ ........ return true; }
You may find that line numbers are not printed with your stack trace. It will simply say "compiled code." To get line numbers, disable the JIT compiler with either the nojit option or with the command-line argument Djava.compiler=NONE. However, it is not as important to get the line numbers if you get the name of the method and the class it belongs to.
Diagnostic methods
The Java language provides methods from the Runtime() class to trace the calls you make to the Java virtual machine. These traces will produce a list of every method call your program makes to the JVM bytecodes. Note that this list can produce a large amount of output, so use it for small sections of your code at a time. Turn on tracing by adding the following line to your source code:
traceMethodCalls(true)
Turn off tracing by adding the following line to your source code:
traceMethodCalls(false)
Start the JVM and watch the output as it is sent to standard output.
Java debugging
Page 10
ibm.com/developerWorks
Java debugging
Page 11
ibm.com/developerWorks
return false; } }
Set the debug for the plot tool or the background or for the entire system by entering the following command line:
java -DplotTool.Debug=true -DbackgroundLoader.Debug=true -Dsystem.Debug=false MapSystem
The statement above states that debugging is turned on for the plotTool application and the background display application but turned off for the entire system. The application keys for each of the applications are: * * * The plotTool application key is plotTool.Debug. The background displayer application key is backgroundLoader.Debug. The entire system application key is system.Debug.
Your actual code would be wrappered with the statement if(debug). If debug is true then the code that is wrappered would execute, thus printing out the results of the System.out.println() methods. To summarize, using if(debug) is recommended only if you are debugging a small amount of code. Create the DebugManager() as a separate compiled class that you can use on the fly when you need to.
Java debugging
Page 12
ibm.com/developerWorks
technique to remote debugging to test cross-platform compatibilities. You can verify that an application runs correctly with different JVMs. This technique also lets you ensure that all of the required system resources are available on another machine. Some debuggers allow you to attach to multiple JVMs and doing so becomes invaluable when you are debugging a multitiered system. As a tier becomes active, the debugger attaches to its JVM and begins examining it thoroughly. Attach the debugger to an already running program or a running JVM where an error or failure occurred. There are two main reasons for using this method of debugging: * * You anticipate a problem at a particular point in your program, and you do not want to step through the program or set breakpoints. You are developing or maintaining a program that hangs sporadically, and you want to find out why it is hanging. In this situation, you can attach the debugger, and look for infinite loops or other problems that might be causing your program to hang.
The JVM you are attaching to must be running in debug mode. It can either be on a remote or local system. The source files can be on either system.
Remote debugging
Imagine that a program running on another user's system is behaving differently than when it runs on your own system. From your system, use the remote debug feature to debug the remote program on the other system. Remote debugging entails running an application on one computer and debugging it on another computer. Remote debugging becomes very useful when debugging complex GUI applications. The debugging facility itself is used exactly as if you were debugging code running locally. However, setup and monitoring the debugging session are a little more complex. Refer to your debugger's documentation to see if it supports remote debugging and to see how to set up the environment. The main requirement for remote debugging is access to networked machines. The class files you are debugging must reside on the remote machine and should have been compiled for debugging. With a GUI debugger, the client user interface is running on one system while the debug engine is running on another system. There are two types of remote debugging: homogeneous and heterogeneous. Homogeneous remote debugging occurs when both the local and remote systems use the same operating system. Heterogeneous remote debugging is when the local and remote systems use different operating systems. Why would you use remote debugging? * It is easier to debug an application that uses graphics or has a graphical user interface
Java debugging
Page 13
ibm.com/developerWorks
when you keep the debugger user interface separate from that of the application. Your interaction with the application occurs on the remote system, while your interaction with the debugger occurs on the local system. The program you are debugging was compiled for a platform that the debugger user interface does not run on. You can use the remote debug feature to take advantage of the debugger user interface while debugging the remote application.
Debugging on demand
Debug on demand enables you to open a debugging session whenever an unhandled exception or other unrecoverable error occurs in your program. The debugger starts and attaches to your program at the point of fault. Doing so can save you time in two ways: you do not have to re-create errors, and your program can run at full speed without interference from the debugger until the fault is encountered. With debug on demand, you can find and fix a problem in your application and let the application continue running.
Java debugging
Page 14
ibm.com/developerWorks
Debugging servlets
When servlets were introduced, many debuggers did not know how to handle them because they cannot be run outside of a Web server. It was not practical to load the entire server into the debugger environment. But debugger support for servlet technology has changed. To debug a servlet, you would run sun.servlet.http.HTTPServer(), then watch as HTTPServer() executes servlets in response to user requests. Note that when debugging servlets, one instance of the servlet is instantiated; however, a new thread is created for each request of a servlet. This is a breeding ground for threading errors. You can use these tools to debug servlets: * * Servletrunner is a free debugging tool from Sun that comes with the JSDK. JDB is another free command-line debugger that ships with the JDK.
Alan R. Williamson presents an alternative in his book Java Servlets By Example (see Resources on page 34). His technique requires you to create your own Debug() class with only static methods. Call these methods in your code only when you need to debug. He suggests that using the Debug() class process:
Java debugging
Page 15
ibm.com/developerWorks
* *
* *
Allows you to print the stack trace when it is needed. Dumps variables of a specific bean by using introspection. The context given to a bean supports the toString() method, so the contents of the bean can be printed out to get an idea of the transactional state of a bean instance. Uses a threshold level to use different debug levels and show which class or method the debugging information came from. Uses System.out.println(), which is the least intrusive in terms of how it affects scheduling. IDEs slow the debugging process down too much to seriously debug concurrency bugs.
As previously mentioned, servlets are instantiated once and then a separate thread is created for each execution request. This process leads into multithreading issues and complicates further the debugging process. Refer to Section 6, "Multithreaded debugging," for more information on debugging servlets.
If you notice that a .java file exists without a companion .class file in the directory where JSP stores its files, then check the .java file for errors. * The last layer actually runs the servlet. Because the JSP engine places your servlet code in one large try/catch block, you may get an exception error. The drawback is that all
Java debugging
Page 16
ibm.com/developerWorks
exceptions are caught under a general RuntimeException() superclass; therefore, you can't determine the specific exceptions that were generated. To get around this problem, insert out.flush() in your code to send output to the browser. Other run-time errors can produce unexpected results, which may be due to incorrect input data or incorrect formatting. When you make any changes to your JSP code after a successful run of the original code, you should delete the .java and .class file in the JSP engine work directory to ensure that you are running the latest code when you test your changes. Because JSP files are compiled into servlets, you will run into the issues of multithreading. JSP files offer an alternative to using the synchronize() keyword: the SingleThreadModel. Simply enter on the page directive:
<%@pageisThreadSafe="false"%>.
* *
Here are some tips: * * * * Some EJB containers have debugging environments that you can use to step through your code in real time. Use a database's log file to help trace problems. Take a look at the generated Java files. Some container tools allow you to keep them around even after compiling them into binary classes. If all else fails, using System.out.println() is the least intrusive way to debug EJB concurrency problems.
Java debugging
Page 17
ibm.com/developerWorks
Java debugging
Page 18
ibm.com/developerWorks
don't use too many synchronized calls, because they directly affect code performance. Synchronizing actually stops multithreading. A code example of using the synchronized method appears below. This code fragment adds an element to a table, resetting the maximum column size of the table in instance variables. You can see the potential for corruption with multiple threads updating the same variable values. Using the synchronized method helps to alleviate this problem.
/** Synchronized method to add an element to a table **/ public void addElement( Patient newPatient ) { synchronized ( lock ) { Log query = incomingPatient.getLog(); results = query.getAllSearchResults(); for ( int k = 0; k < results.length; k++) { .... // add to table setMaxColSize(MyTable); tableContents.add(MyTable); } } }
* * *
Java debugging
Page 19
ibm.com/developerWorks
The Jikes project has been ported to the Windows, Linux, UNIX, and Macintosh platforms.
Jikes toolset
Jikes contains a Java source-to-bytecode compiler and GUI debugger. * * The compiler is invoked by the jikes command The debugger is invoked by the jd command
Java debugging
Page 20
ibm.com/developerWorks
debugger. Unfortunately, Jikes' user interface is limited. It doesn't remember breakpoints and other settings between sessions, and all actions are mouse-driven. Because it is open-source code, users are encouraged to add enhancements and share them with the developer community.
The panels are organized as follows: * Locals displays the variables local to the current stack trace. * Callers displays the stack trace or the path the executable code has taken. * Threads displays the threads that are currently running or waiting to execute. * Classes displays the classes within your program. * Inspector displays the values of the local variables. * Console displays any I/O or error streams. This is where you would enter input or read output. * Source editor displays the source code for the class currently executed. This panel is where you will spend most of your time.
Java debugging
Page 21
ibm.com/developerWorks
To begin debugging
1. First compile your program using:
javac -g LinkTree.java
2.
The debugger starts and loads the class LinkTree, which contains the main() method, setting it up for debugging. You have the option of passing in parameters (such as a URL to start printing page links) by entering them on the command line following the command jd. You should see the following in the debugger window:
ibm.com/developerWorks
The Console panel displays any run-time exceptions. All error streams are printed here. This is also where you can enter input when your application requests it. Textual information is printed to the Console panel. In the graphic below, we are printing the links as they are recursively found within the HTML page. You will see the links and at what depth they were found.
Java debugging
Page 23
ibm.com/developerWorks
Your Source editor window should display the code with the following breakpoint. See the graphic below.
Run the application and watch it stop right at the line containing the breakpoint. You can perform more in-depth analysis of what the code is doing. We will look at examining variables in the next panel. Note that after examining your variables,you can step line-by-line or
Java debugging
Page 24
ibm.com/developerWorks
continue full, uninterrupted execution by using the Run or Step Into buttons.
Java debugging
Page 25
ibm.com/developerWorks
Java debugging
Page 26
ibm.com/developerWorks
Java debugging
Page 27
ibm.com/developerWorks
More on JDB
JDB can be configured for debugging multiple projects. JDB looks for a jdb.ini configuration file in the user.home directory. Therefore, you should set the user.home property to point to a different .ini file in another directory for each project. You can do this by entering the command:
jdb -J-Duser.home=. //Will look in the current directory for the jdb.ini file
The jdb.ini file can start a JDB session, pass in parameters, and display information about the system. Below is an example of a jdb.ini file. It includes the Java platform sources on the source path list and passes in the parameter 34 to the program. It then runs and stops at line 2 and displays the free memory and waits for further input.
load MyTest stop at MyTest:20 use c:/java;c:/jdk/src run MyTest 34 memory
You can record your debugging sessions with JDB. Enable logging by creating a file called .agentLog in the directory where you are running JDB. In the .agentLog file, put the filename that you want the session information to be written to on the first line. When you run the jdb command, you will see jdb session information. The log file could contain the following:
-------- debug agent message log -------[debug agent: adding Debugger agent to system thread list] [debug agent: adding Breakpoint handler to system thread list] [debug agent: no such class: MyApplication.main]
Java debugging
Page 28
ibm.com/developerWorks
engine. Tomcat is a JSP/servlet engine that interfaces well with JDB. Other servlet engines that are Window application .exe files can't run in debug mode. However, Tomcat has a startup script that allows you to configure it for debugging. You need to configure the TOMCAT_OPTS environment variable. Running an engine in debug mode also affects the CLASSPATH. Set the boot classpath with -Xbootclasspath with the path for the rt.jar and the tools.jar files found in JDK2.0. Alternatively, you can debug using Servletrunner. Servletrunner is an application that comes with the JSDK. It lets you run a servlet without a Web browser. Servletrunner runs as a Java Web server that is lighter than the Java Web server. It handles HTTP requests for servlets. Servletrunner runs the command: java sun.servlet.http.HttpServer. You should run the jdb session with the HttpServer() class. When you are debugging servlets, keep in mind that the Java Web server and Servletrunner do not load and unload servlets by including the servlets directory on the CLASSPATH. Instead, they use a custom classloader and not the default system classloader.
Java debugging
Page 29
ibm.com/developerWorks
First you make preparations for running a servlet engine. You can use Tomcat or Servletrunner. To prepare to run the servlet engine, configure the CLASSPATH. The CLASSPATH needs to find sun.servlet.http.HttpServer and its associated classes. Also, the CLASSPATH needs to find the servlet file tools.jar in the /lib directory. You won't see any GUI with this exercise because JDB is a command-line interface.
2. 3.
Java debugging
Page 30
ibm.com/developerWorks
Initializing jdb...
4.
The HTTPServer class contains the main() method that listens for incoming requests for servlets.
Again, there is nothing wrong with the code, we simply chose to use it in our case study.
Setting breakpoints
Next, set a breakpoint in the doGet() method of MyHelloWorld.
> stop in MyHelloWorld.doGet Breakpoint set in MyHelloWorld.doGet > run run sun.servlet.http.HttpServer running ...
The debugger will stop at the breakpoint at the beginning of the method doGet(). You will see this in the DOS window where you started the debugger. You can use the list command to see where in the servlet the debugger has stopped.
Java debugging
Page 31
ibm.com/developerWorks
This simple exercise ran JDB against a servlet. After you become familiar with the commands that JDB uses to debug code, you will better understand what GUI debug tools that interface to JDB are doing under the hood. JDB is also great when you need to debug very small pieces of code. It is a quick and dirty way to get right to the source of an application's problem.
Java debugging
Page 32
ibm.com/developerWorks
Section 11. Hints and tips General debugging hints and tips
* * When you create your own beans, add a public static void main(String[] args) method to it. This allows easy debugging of each component separately. If you ever find yourself building a debugger or even a simple class to handle debugging, keep in mind that its performance is also a key issue. You don't want to ship production code with a debugger that is a performance hog. Modularizing your code into individual classes, beans, and EJB components makes debugging easier. You could potentially test code separately. You can use Reflection classes to debug JavaBean components via introspection. Format your code by lining up your braces and lining up your if/then/else clauses. This helps in the debugging process. Do not tune your code for performance until you completely debug it. Do not put multiple statements on the same line because some debugger features operate on a line-by-line basis. For example, you cannot step over or set line breakpoints on more than one statement on a line. Assign intermediate expression values to temporary variables to make it easier to verify intermediate results. Because some bugs hide other bugs, start debugging at the top of your program and work your way down. Use try/catch blocks frequently to catch and report errors and exceptions.
* * * * *
* * *
Java debugging
Page 33
ibm.com/developerWorks
Resources
* * * * * * * * * * * "Bug patterns," by Eric Allen, is the first in a series of articles that helps you diagnose and correct recurring bug types in your Java programs. See "Techniques for adding trace statements to your Java application," by Andrei Malacinski, for a way to squash bugs during development as well as after deployment. "Java Pitfalls," by Michael Daconta, offers suggestions on techniques for debugging, as does "Java Servlets by Example," by Alan R. Williamson. Debugging Java by Will David Mitchell offers more trouble-shooting techniques. Visit the Jikes Home page where you can learn more about the Jikes Project and download the code. Download the Jikes debugger (JD) from IBM alphaWorks . Get more information on Jinsight and download the code from IBM alphaWorks . The March 2001 issue of "Dr. Dobb's Journal" focuses on testing and debugging. Check out the poll results on the developerWorks Java technology zone that show how developers debug code and whether they had formal training in debugging . Find everything you want to know about Java servlet technology , including links to the Servletrunner and Tomcat engines. Learn more about what is happening in the wireless arena at the Global Embedded Processor Debug Interface Forum Web site.
Your feedback
Java debugging Page 34
ibm.com/developerWorks
Please let us know whether this tutorial was helpful to you and how we could make it better. We'd also like to hear about other tutorial topics you'd like to see covered. Thanks! For questions about the content of this tutorial, contact the author, Laura Bennett, at [email protected] .
Java debugging
Page 35
ibm.com/developerWorks
public static void main(String[] args) { // Read in the root url LinksTree lt = new LinksTree("index.html"); lt.listAll(0); }
Java debugging
Page 36
ibm.com/developerWorks
private Vector parseHTMLContainsLinks(String urlLink) { EditorKit kit = new HTMLEditorKit(); Document doc = (HTMLDocument)kit.createDefaultDocument(); // clear out the vector htmlElements.removeAllElements(); // The Document class does not yet // handle charset's properly. doc.putProperty("IgnoreCharsetDirective", Boolean.TRUE); try { // Create a reader on the HTML content. Reader rd = getReader(getName()); // Parse the HTML. kit.read(rd, doc, 0); // Iterate through the elements // of the HTML document. ElementIterator it = new ElementIterator(doc); javax.swing.text.Element elem; while ((elem = it.next()) != null) { SimpleAttributeSet s = (SimpleAttributeSet) elem.getAttributes().getAttribute(HTML.Tag.A); if (s != null) { if ((s.getAttribute(HTML.Attribute.HREF)).toString().startsWith("/")) { String relativeURL = (s.getAttribute(HTML.Attribute.HREF)).toString(); if (getName().endsWith("/")) htmlElements.addElement(getName() + relativeURL.substring(1)); else htmlElements.addElement(getName() + relativeURL.substring(0)); } else { htmlElements.addElement((s.getAttribute(HTML.Attribute.HREF)).toString()); } } } } catch (Exception e) { e.printStackTrace(); System.exit(1); } return htmlElements; //System.exit(1); } // end of main // Returns a reader on the HTML data. If 'uri' begins // with "http:", it's treated as a URL; otherwise, // it's assumed to be a local filename. static Reader getReader(String uri) throws IOException { if (uri.startsWith("http:")) { // Retrieve from Internet. URLConnection conn = new URL(uri).openConnection(); return new InputStreamReader(conn.getInputStream()); } else { // Retrieve from file. return new FileReader(uri); } } // end of getReader }
Java debugging
Page 37
ibm.com/developerWorks
Colophon
This tutorial was written entirely in XML, using the developerWorks Toot-O-Matic tutorial generator. The Toot-O-Matic tool is a short Java program that uses XSLT stylesheets to convert the XML source into a number of HTML pages, a zip file, JPEG heading graphics, and two PDF files. Our ability to generate multiple text and binary formats from a single source file illustrates the power and flexibility of XML.
Java debugging
Page 38