In this article we will go through very simple Hello World example for Java Debug Interface API i.e. JDI. This can help for getting started with JDI. We will also try to understand how JDI works in very basic manner.
Objective: We will debug Hello world program using JDI & read its variable & print. In the process, we will try to understand how basic JDI works.
JDI Terminologies:
- Debugger program – Code/program which will be debugging other code/program.
- Debug target program – Code/program which is being debugged.
- Virtual Machine (VM) – JVM which is running debug target program.
- Connector – This connects debugger program to JVM of debug target. In this example we will use LaunchingConnector which will launch a JVM & connect to it. There are also AttachingConnector which connect to existing running JVM.
- Events – When VM runs debug target program in debug mode, it triggers several events so that debugger program can take action as needed. Debugger program can also request VM to trigger certain special events which are not triggered by default.
Steps:
- Create Virtual Machine using launching connector which has ability to launch a VM & connect to it.
- Request VM to let program know when class (debug target) is prepared. VM will trigger ClassPrepareEvent when class is prepared so that program knows.
- When ClassPrepareEvent is triggered, request VM to set a breakpoint at expected line number. When code execution reached that line number, VM will trigger BreakpointEvent.
- Keep looping till VM reaches to the breakpoint line & triggers BreakpointEvent
- When BreakpointEvent received, disable breakpoint, read local variables & resume VM.
Code:
This is the debug target class which we will debug pragmatically using JDI. We will be putting debug breakpoint at line denoted by a comment.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package com.itsallbinary.java.jdi; /** * Debug this program with JDI & read all local variables. * * @author ravik * */ public class HelloWorld { public static void main(String[] args) { String helloWorld = "Hello World. "; String welcome = "Welcome to Its All Binary !"; String greeting = helloWorld + welcome; System.out.println("Hi Everyone, " + greeting);// Put a break point at this line. } } |
This is the Debugger program which will debug above program.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
package com.itsallbinary.java.jdi; import java.util.Map; import com.sun.jdi.Bootstrap; import com.sun.jdi.ClassType; import com.sun.jdi.LocalVariable; import com.sun.jdi.Location; import com.sun.jdi.StackFrame; import com.sun.jdi.VMDisconnectedException; import com.sun.jdi.Value; import com.sun.jdi.VirtualMachine; import com.sun.jdi.connect.Connector; import com.sun.jdi.connect.LaunchingConnector; import com.sun.jdi.event.BreakpointEvent; import com.sun.jdi.event.ClassPrepareEvent; import com.sun.jdi.event.Event; import com.sun.jdi.event.EventSet; import com.sun.jdi.request.BreakpointRequest; import com.sun.jdi.request.ClassPrepareRequest; /** * Hello world example for Java Debugging API i.e. JDI. Very basic & simple * example. * * @author ravik * */ public class HelloWorldJDIExample { public static void main(String[] args) throws Exception { Class classToDebug = HelloWorld.class; int lineNumberToPutBreakpoint = 18; /* * Prepare connector, set class to debug & launch VM. */ LaunchingConnector launchingConnector = Bootstrap.virtualMachineManager().defaultConnector(); Map<String, Connector.Argument> env = launchingConnector.defaultArguments(); env.get("main").setValue(classToDebug.getName()); VirtualMachine vm = launchingConnector.launch(env); /* * Request VM to trigger event when HelloWorld class is prepared. */ ClassPrepareRequest classPrepareRequest = vm.eventRequestManager().createClassPrepareRequest(); classPrepareRequest.addClassFilter(classToDebug.getName()); classPrepareRequest.enable(); EventSet eventSet = null; try { while ((eventSet = vm.eventQueue().remove(100)) != null) { for (Event event : eventSet) { /* * If this is ClassPrepareEvent, then set breakpoint */ if (event instanceof ClassPrepareEvent) { ClassPrepareEvent evt = (ClassPrepareEvent) event; ClassType classType = (ClassType) evt.referenceType(); Location location = classType.locationsOfLine(lineNumberToPutBreakpoint).get(0); BreakpointRequest bpReq = vm.eventRequestManager().createBreakpointRequest(location); bpReq.enable(); } /* * If this is BreakpointEvent, then read & print variables. */ if (event instanceof BreakpointEvent) { // disable the breakpoint event event.request().disable(); // Get values of all variables that are visible and print StackFrame stackFrame = ((BreakpointEvent) event).thread().frame(0); Map<LocalVariable, Value> visibleVariables = (Map<LocalVariable, Value>) stackFrame .getValues(stackFrame.visibleVariables()); System.out.println("Local Variables ="); for (Map.Entry<LocalVariable, Value> entry : visibleVariables.entrySet()) { System.out.println(" " + entry.getKey().name() + " = " + entry.getValue()); } } vm.resume(); } } } catch (VMDisconnectedException e) { System.out.println("VM is now disconnected."); } catch (Exception e) { e.printStackTrace(); } } } |
Compilation:
1 |
Java_Tutorials\src\main\java>"C:\Program Files\Java\jdk1.8.0_121\bin\javac" -g -cp "C:\Program Files\Java\jdk1.8.0_121\lib\tools.jar" com\itsallbinary\java\jdi\*.java |
Please take below precautions to avoid any issues in execution of example.
- Make sure to use -g option during compilation. This option generates all debugging information, including local variables. If you miss this, you might see AbsentInformationException as below. Oracle Reference
-
123456com.sun.jdi.AbsentInformationExceptionat com.sun.tools.jdi.ConcreteMethodImpl.getVariables1(ConcreteMethodImpl.java:495)at com.sun.tools.jdi.ConcreteMethodImpl.getVariables(ConcreteMethodImpl.java:540)at com.sun.tools.jdi.ConcreteMethodImpl.variables(ConcreteMethodImpl.java:217)at com.sun.tools.jdi.StackFrameImpl.createVisibleVariables(StackFrameImpl.java:159)at com.sun.tools.jdi.StackFrameImpl.visibleVariableByName(StackFrameImpl.java:194)
-
- Make sure to have jdk\lib\tools.jar in classpath as it has all JDI Library. Without this you might get java.lang.ClassNotFoundException: com.sun.jdi.Bootstrap
Execution:
1 2 3 4 5 6 7 |
Java_Tutorials\src\main\java>"C:\Program Files\Java\jdk1.8.0_121\bin\java" -cp ".;C:\Program Files\Java\jdk1.8.0_121\lib\tools.jar" com.itsallbinary.java.jdi.HelloWorldJDIExample Local Variables = helloWorld = "Hello World. " args = instance of java.lang.String[0] (id=92) greeting = "Hello World. Welcome to Its All Binary !" welcome = "Welcome to Its All Binary !" VM is now disconnected. |
- Make sure to have jdk\lib\tools.jar in classpath as it has all JDI Library.
- Note “.;” in classpath. If you miss that you might see Error: Could not find or load main class com.itsallbinary.java.jdi.HelloWorldJDIExample
- If you run this program as part of maven project, then classes will be compiled in target directory. So you might need below additional env argument in code of HelloWorldJDIExample.
1 2 |
Connector.Argument options = (Connector.Argument) env.get("options"); options.setValue("-cp \"C:\\<path-to-project>\\<project-name>\\target\\classes\""); |
As you can see in execution output, HelloWorld.java program has been executed & all local variable names & their values are printed in the logs.
Where is System.out.println from HelloWorld.java?
As you might have noticed in execution output, that the output of System.out.println("Hi Everyone, " + greeting);// Put a break point at this line. is not printed in console even though program has been executed correctly. Why is so?
If you check the API javadoc of LaunchingConnector.html#launch, it has below documentation.
Important note: If a target VM is launched through this functions, its output and error streams must be read as it executes. These streams are available through the
Process
object returned byVirtualMachine.process()
. If the streams are not periodically read, the target VM will stop executing when the buffers for these streams are filled.
So we have to get the streams from process & connect it to System.out of debugger program. Ideally this mush be done in a separate parallel thread which can be spun from debugger program & continuously keep reading streams. This way we can meet the requirement from above documentation of reading streams periodically.
For our example, since we have just one small statement printed, we won’t run into situation of buffer filling up & VM getting stopped. So we will just read & print stream at the end of program in finally block.
1 2 3 4 5 6 7 8 9 |
finally { InputStreamReader reader = new InputStreamReader(vm.process().getInputStream()); OutputStreamWriter writer = new OutputStreamWriter(System.out); char[] buf = new char[1024]; reader.read(buf); writer.write(buf); writer.flush(); } |
Complete code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
package com.itsallbinary.java.jdi; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.util.Map; import com.sun.jdi.Bootstrap; import com.sun.jdi.ClassType; import com.sun.jdi.LocalVariable; import com.sun.jdi.Location; import com.sun.jdi.StackFrame; import com.sun.jdi.VMDisconnectedException; import com.sun.jdi.Value; import com.sun.jdi.VirtualMachine; import com.sun.jdi.connect.Connector; import com.sun.jdi.connect.LaunchingConnector; import com.sun.jdi.event.BreakpointEvent; import com.sun.jdi.event.ClassPrepareEvent; import com.sun.jdi.event.Event; import com.sun.jdi.event.EventSet; import com.sun.jdi.request.BreakpointRequest; import com.sun.jdi.request.ClassPrepareRequest; /** * Hello world example for Java Debugging API i.e. JDI. Very basic & simple * example. * * @author ravik * */ public class HelloWorldJDIExample { public static void main(String[] args) throws Exception { Class classToDebug = HelloWorld.class; int lineNumberToPutBreakpoint = 18; /* * Prepare connector, set class to debug & launch VM. */ LaunchingConnector launchingConnector = Bootstrap.virtualMachineManager().defaultConnector(); Map<String, Connector.Argument> env = launchingConnector.defaultArguments(); env.get("main").setValue(classToDebug.getName()); VirtualMachine vm = launchingConnector.launch(env); /* * Request VM to trigger event when HelloWorld class is prepared. */ ClassPrepareRequest classPrepareRequest = vm.eventRequestManager().createClassPrepareRequest(); classPrepareRequest.addClassFilter(classToDebug.getName()); classPrepareRequest.enable(); EventSet eventSet = null; try { while ((eventSet = vm.eventQueue().remove(100)) != null) { for (Event event : eventSet) { /* * If this is ClassPrepareEvent, then set breakpoint */ if (event instanceof ClassPrepareEvent) { ClassPrepareEvent evt = (ClassPrepareEvent) event; ClassType classType = (ClassType) evt.referenceType(); Location location = classType.locationsOfLine(lineNumberToPutBreakpoint).get(0); BreakpointRequest bpReq = vm.eventRequestManager().createBreakpointRequest(location); bpReq.enable(); } /* * If this is BreakpointEvent, then read & print variables. */ if (event instanceof BreakpointEvent) { // disable the breakpoint event event.request().disable(); // Get values of all variables that are visible and print StackFrame stackFrame = ((BreakpointEvent) event).thread().frame(0); Map<LocalVariable, Value> visibleVariables = (Map<LocalVariable, Value>) stackFrame .getValues(stackFrame.visibleVariables()); System.out.println("Local Variables ="); for (Map.Entry<LocalVariable, Value> entry : visibleVariables.entrySet()) { System.out.println(" " + entry.getKey().name() + " = " + entry.getValue()); } } vm.resume(); } } } catch (VMDisconnectedException e) { System.out.println("VM is now disconnected."); } catch (Exception e) { e.printStackTrace(); } finally { InputStreamReader reader = new InputStreamReader(vm.process().getInputStream()); OutputStreamWriter writer = new OutputStreamWriter(System.out); char[] buf = new char[1024]; reader.read(buf); writer.write(buf); writer.flush(); } } } |
1 2 3 4 5 6 7 8 |
Java_Tutorials\src\main\java>"C:\Program Files\Java\jdk1.8.0_121\bin\java" -cp ".;C:\Program Files\Java\jdk1.8.0_121\lib\tools.jar" com.itsallbinary.java.jdi.HelloWorldJDIExample Local Variables = greeting = "Hello World. Welcome to Its All Binary !" welcome = "Welcome to Its All Binary !" helloWorld = "Hello World. " args = instance of java.lang.String[0] (id=92) VM is now disconnected. Hi Everyone, Hello World. Welcome to Its All Binary ! |
Now you can see statement from HelloWorld.java being printed in console.
Further Reading:
Go through below articles for further understanding of Java Debug Interface in depth.
Java Debug Interface API (JDI) – Hello World Example | Programmatic stepping through the code lines
Very good information, thank you very much!
Is there any way to programmatically expand or see the values that are in the “instance of” fields?
Thanks
Thank you, how do i run the above code in eclipse ide. I try to run the above code and i get “VM is disconnected”.