Tuesday, April 19, 2011

JDB - Command line debugger for Java

Introduction


Debugger is an unavoidable tool in any Java developer’s toolkit. A Debugger helps when the application/module/function behaviors different than that of developers expectation. Typically developer will first try to understand difference in behavior by glancing through the code quickly. If he still not able to understand the reason for the unexpected behavior; next step would be step through the program execution using a debugger. I assume readers are familiar with the normal debugging procedures. If not please go through the appendix.
Most of the current debuggers are GUI based (either integrated with IDEs (IntelliJ, Eclipse) or stand alone debuggers (Yourkit).
However GUI based debuggers are not very useful when the application is running on a remote machine, especially when the connectivity between the machine where appication is running and the machine where debugger is running is poor.
In such scenarios a command line debugger which is also running on the same box as the application is running would be useful.

JDB – a free command line debugger


JDB is a free command line debugger. JDB is part of JDK and is supported by Sun JDK on various Operating systems like Windows, Linux, Solaris and Mac OS x.
How to use
  1. We need to run the application with ‘debug enabled’. You need to provide the following JVM arguments ‘-Xdebug –Xrunjdwp:transport=st_socket,server=y,address=8000,suspend=y’ to start the application in debug mode. Please refer to http://download.oracle.com/javase/1.5.0/docs/guide/jpda/conninv.html#Invocation for further details.
  2. Application should be compiled with –g option.
  3. Attach JDB to the application ‘jdb –attach 8000’
  4. You can see the list commands by typing ‘help’
  5. Set breakpoint at some significant point using ‘stop at :
  6. Navigate through the code step by step using ‘step’ and ‘next’ and other commands.
Given below are a list of important commands
  1. stop at : - to set breakpoint
  2. run – to start execution or resume from a breakpoint
  3. locals – list values of local variables
  4. dump this – list variables of the instance ‘this’
  5. where all – to see the thread dump
  6. list – to see current code
  7. step – execute one line (if it is function step into the function)
  8. next – execute one line (if it is a function step over the function)
  9. help – print list of available commands with description
To see the complete list of commands type ‘help’ command.
Let us see an example to illustrate this further.

Example – Debugging Jackson (JSON processing tool)


Jackson is tool for parsing JSON (from JSON to object and Object to JSON parsing).
You can download Jackson source from http://jackson.codehaus.org/1.7.6/jackson-src-1.7.6.zip
Unzip the contents to any convenient location (. Build the source using ‘ant’.
ant jars
All packages shall be generated as /build. We need jackson-mapper-asl-1.7.6.jar and jackson-core-asl-1.7.6.jar
Given below is a small code which converts a Java object to JSON string.
We will navigate through the Jackson processing using debugger.


import org.codehaus.jackson.map.ObjectMapper;
public class Main {
public static void main(String[] args) throws Exception {
System.out.println("entering the program....");
MyEntity obj = new MyEntity();
obj.setId(1);
obj.setName("rejeev");
obj.setAddress("c-48, sree nagar");
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(obj);
System.out.println("json: " + json);
}
}

public class MyEntity {
private int id;
private String name;
private int age;
private String address;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}


Save this code at
Compile the classes:

javac –g –cp ./build/jackson-mapper-asl-1.7.6.jar:./build/jackson-core-asl-1.7.6.jar:. –d . MyEntity.java Main.java

Run the application in debug mode.

java –Xbebug –Xrunjdwp:transport =st_socket,server=y,address=8000,suspend=y –cp ./build/Jackson-mapper-asl-1.7.6.jar:./build/Jackson-core-asl-1.7.6.jar:. Main

You will get the following output:

Listening for transport dt_socket at address: 8000

As the option ‘suspend=y’ is specified, the JVM is just started. Method exectution is not yet started. When we trigger ‘run’ command from debugger exectution will start.

Attach debugger to the application

jdb –sourcepath :/src/mapper/java –attach 8000

You will get the following output:

Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
Initializing jdb ...
>
VM Started: No frames on the current call stack
main[1]

To start the execution run the command ‘step’

main[1] step
>
Step completed: "thread=main", Main.main(), line=4 bci=0
4 System.out.println("entering the program....");

Now program is at the first line of the function ‘main’
Let us see the code around the current line of execution.

main[1] list
1 import org.codehaus.jackson.map.ObjectMapper;
2 public class Main {
3 public static void main(String[] args) throws Exception {
4 => System.out.println("entering the program....");
5 MyEntity obj = new MyEntity();
6 obj.setId(1);
7 obj.setName("rejeev");
8 obj.setAddress("c-48, sree nagar");
9 ObjectMapper mapper = new ObjectMapper();
10 String json = mapper.writeValueAsString(obj);

The arrow indicates the current line of execution.
Open the java source files (Main, MyEntity and Jackson source files) in a TextEditor or IDE.
Let us start from Main. Main:10 is the line which parse object to JSON string.
Let us set breakpoint at Main:10

main[1] stop at Main:10
Set breakpoint Main:10

To move the execution from the current line to next breakpoint (line 10)

main[1]
main[1] run
Breakpoint hit: "thread=main", Main.main(), line=10 bci=41
10 String json = mapper.writeValueAsString(obj);

To see the code around current line

main[1] list
6 obj.setId(1);
7 obj.setName("rejeev");
8 obj.setAddress("c-48, sree nagar");
9 ObjectMapper mapper = new ObjectMapper();
10 => String json = mapper.writeValueAsString(obj);
11 System.out.println("json: " + json);
12 }
13 }


We can see all local variables using ‘locals’

main[1] locals
Method arguments:
args = instance of java.lang.String[0] (id=867)
Local variables:
obj = instance of MyEntity(id=868)
mapper = instance of org.codehaus.jackson.map.ObjectMapper(id=869)

Now we can inspect the content of any object. Let us inspect ‘obj’.

main[1] dump obj
obj = {
id: 1
name: "rejeev"
age: 0
address: "c-48, sree nagar"
}


Now let us step into ‘writeValueAsString’ using the command ‘step’

main[1] step
>
Step completed: "thread=main", org.codehaus.jackson.map.ObjectMapper.writeValueAsString(), line=1,595 bci=0

Now see the current code again using the command ‘list’

main[1] list
1,591 public String writeValueAsString(Object value)
1,592 throws IOException, JsonGenerationException, JsonMappingException
1,593 {
1,594 // alas, we have to pull the recycler directly here...
1,595 => SegmentedStringWriter sw = new SegmentedStringWriter(_jsonFactory._getBufferRecycler());
1,596 _configAndWriteValue(_jsonFactory.createJsonGenerator(sw), value);
1,597 return sw.getAndClear();
1,598 }

Let us go to the next line using the command ‘next’ and from line 1596, let us step into the function ‘_configAndWriteValue’

main[1] step
>
Step completed: "thread=main", org.codehaus.jackson.JsonFactory.createJsonGenerator(), line=489 bci=0
489 IOContext ctxt = _createContext(out, false);

Instead of stepping into _configAndWriteValue, we are now in JsonFactory.createGenerator. This is because first method parameters are calculated. To get into _configAndWriteValue, let us get out of createJsonGenerator using step up.

main[1] step up
>
Step completed: "thread=main", org.codehaus.jackson.map.ObjectMapper.writeValueAsString(), line=1,596 bci=24
1,596 _configAndWriteValue(_jsonFactory.createJsonGenerator(sw), value);

Now we are back at line no: 1596. Let us invoke step again.

main[1] step
>
Step completed: "thread=main", org.codehaus.jackson.map.ObjectMapper._configAndWriteValue(), line=1,982 bci=0
1,982 SerializationConfig cfg = copySerializationConfig();

Let us see the code around this

main[1] list
1,978 */
1,979 protected final void _configAndWriteValue(JsonGenerator jgen, Object value)
1,980 throws IOException, JsonGenerationException, JsonMappingException
1,981 {
1,982 => SerializationConfig cfg = copySerializationConfig();
1,983 // [JACKSON-96]: allow enabling pretty printing for ObjectMapper directly
1,984 if (cfg.isEnabled(SerializationConfig.Feature.INDENT_OUTPUT)) {
1,985 jgen.useDefaultPrettyPrinter();
1,986 }

Using this approach we can navigate around the code.
At any point we can see the thread dump using ‘where all’

main[1] where all
Signal Dispatcher:
Finalizer:
[1] java.lang.Object.wait (native method)
[2] java.lang.ref.ReferenceQueue.remove (ReferenceQueue.java:118)
[3] java.lang.ref.ReferenceQueue.remove (ReferenceQueue.java:134)
[4] java.lang.ref.Finalizer$FinalizerThread.run (Finalizer.java:159)
Reference Handler:
[1] java.lang.Object.wait (native method)
[2] java.lang.Object.wait (Object.java:485)
[3] java.lang.ref.Reference$ReferenceHandler.run (Reference.java:116)
main:
[1] org.codehaus.jackson.map.ObjectMapper._configAndWriteValue (ObjectMapper.java:1,982)
[2] org.codehaus.jackson.map.ObjectMapper.writeValueAsString (ObjectMapper.java:1,596)
[3] Main.main (Main.java:10)

Appendices:


Basic concept of debugging


A Debugger helps when the application/module/function behaviors different than that of developers expectation. Typically developer will first try to understand difference in behavior by glancing through the code quickly. If he still not able to understand the reason for the unexpected behavior; next step would be step through the program execution using a debugger.
Using a debugger one can put a mark (called breakpoint) on any line in the source code and the program will stop execution when the execution reaches that mark. Then we can do the following:
1. Inspect the state of the program at that point. Typically following states are valuable – (a) Local variables, (b) Instance variables of the current object (this), (b) Execution stack of the request
2. Execute the program from that point (breakpoint), step by step, one line at a time. The current line could be an invocation of a function. In that case we have two options – (a) execute the function as single step and process to next line in the current code segment (step over), (b) step into the function and go to the next line in the function (step in).
3. Continue the execution of program (resume) from the breakpoint (typically to next breakpoint).
Typically we put a breakpoint at a line where we think the beginning of the significant code. Then navigate through the code step by step using either step-over and step-in, also inspecting local variables and instance variables at relevant steps. Once we complete the analysis of one significant code segment, we will move to another significant code segment by putting a breakpoint at the beginning of that segment and using ‘rusume’ to move from old code segment to new code segment.
By this method most of the issues can be explained. However analyzing concurrency issues requires different approach.
Consider a scenario where your program execution is affected by the setting of a Boolean variable which is reset by some other thread which you don’t know.
This can be analyzed by putting a ‘watch’ on that variable. Whenever that variable is modified, execution is stopped at that point, by inspecting the stacktrace, we will be able understand which thread at which code modifies the variable.

1 comment:

javin paul said...

Thanks for explaning about JDB but I have found eclipse remote debugging more easy and convinient then any command line debugger the feature eclipse provides e.g. logical view, conditional breakpoint, exception breakpoint , step over, step into are great , see here on How to setup java remote debugging in Eclipse

Javin