1. Overview
Debugging a remote Java Application can be handy in more than one case.
In this tutorial, we'll discover how to do that using JDK's tooling.
2. The Application
Let's start by writing an application. We'll run it on a remote location and debug it locally through this article:
public class OurApplication { private static String staticString = "Static String"; private String instanceString; public static void main(String[] args) { for (int i = 0; i < 1_000_000_000; i++) { OurApplication app = new OurApplication(i); System.out.println(app.instanceString); } } public OurApplication(int index) { this.instanceString = buildInstanceString(index); } public String buildInstanceString(int number) { return number + ". Instance String !"; } }
3. JDWP: The Java Debug Wire Protocol
The Java Debug Wire Protocol is a protocol used in Java for the communication between a debuggee and a debugger. The debuggee is the application being debugged while the debugger is an application or a process connecting to the application being debugged.
Both applications either run on the same machine or on different machines. We'll focus on the latter.
3.1. JDWP's Options
We'll use JDWP in the JVM command-line arguments when launching the debuggee application.
Its invocation requires a list of options:
- transport is the only fully required option. It defines which transport mechanism to use. dt_shmem only works on Windows and if both processes run on the same machine while dt_socket is compatible with all platforms and allows the processes to run on different machines
- server is not a mandatory option. This flag, when on, defines the way it attaches to the debugger. It either exposes the process through the address defined in the address option. Otherwise, JDWP exposes a default one
- suspend defines whether the JVM should suspend and wait for a debugger to attach or not
- address is the option containing the address, generally a port, exposed by the debuggee. It can also represent an address translated as a string of characters (like javadebug if we use server=y without providing an address on Windows)
3.2. Launch Command
Let's start by launching the remote application. We'll provide all the options listed earlier:
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 OurApplication
Until Java 5, the JVM argument runjdwp had to be used together with the other option debug:
java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000
This way of using JDWP is still supported but will be dropped in future releases. We'll prefer the usage of the newer notation when possible.
3.3. Since Java 9
Finally, one of the options of JDWP has changed with the release of version 9 of Java. This is quite a minor change since it only concerns one option but will make a difference if we're trying to debug a remote application.
This change impacts the way address behaves for remote applications. The older notation address=8000 only applies to localhost. To achieve the old behavior, we'll use an asterisk with a colon as a prefix for the address (e.g address=*:8000).
According to the documentation, this is not secure and it's recommended to specify the debugger's IP address whenever possible:
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=127.0.0.1:8000
4. JDB: The Java Debugger
JDB, the Java Debugger, is a tool included in the JDK conceived to provide a convenient debugger client from the command-line.
To launch JDB, we'll use the attach mode. This mode attaches JDB to a running JVM. Other running modes exist, such as listen or run but are mostly convenient when debugging a locally running application:
jdb -attach 127.0.0.1:8000 > Initializing jdb ...
4.1. Breakpoints
Let's continue by putting some breakpoints in the application presented in section 1.
We'll set a breakpoint on the constructor:
> stop in OurApplication.<init>
We'll set another one in the static method main, using the fully-qualified name of the String class:
> stop in OurApplication.main(java.lang.String[])
Finally, we'll set the last one on the instance method buildInstanceString:
> stop in OurApplication.buildInstanceString(int)
We should now notice the server application stopping and the following being printed in our debugger console:
> Breakpoint hit: "thread=main", OurApplication.<init>(), line=11 bci=0
Let's now add a breakpoint on a specific line, the one where the variable app.instanceString is being printed:
> stop at OurApplication:7
We notice that at is used after stop instead of in when the breakpoint is defined on a specific line.
4.2. Navigate and Evaluate
Now that we've set our breakpoints, let's use cont to continue the execution of our thread until we reach the breakpoint on line 7.
We should see the following printed in the console:
> Breakpoint hit: "thread=main", OurApplication.main(), line=7 bci=17
As a reminder, we've stopped on the line containing the following piece of code:
System.out.println(app.instanceString);
Stopping on this line could have also been done by stopping on the main method and typing step twice. step executes the current line of code and stops the debugger directly on the next line.
Now that we've stopped, the debugee is evaluating our staticString, the app‘s instanceString, the local variable i and finally taking a look at how to evaluate other expressions.
Let's print staticField to the console:
> eval OurApplication.staticString OurApplication.staticString = "Static String"
We explicitly put the name of the class before the static field.
Let's now print the instance field of app:
> eval app.instanceString app.instanceString = "68741. Instance String !"
Next, let's see the variable i:
> print i i = 68741
Unlike the other variables, local variables don't require to specify a class or an instance. We can also see that print has exactly the same behavior as eval: they both evaluate an expression or a variable.
We'll evaluate a new instance of OurApplication for which we've passed an integer as a constructor parameter:
> print new OurApplication(10).instanceString new OurApplication(10).instanceString = "10. Instance String !"
Now that we've evaluated all the variables we needed to, we'll want to delete the breakpoints set earlier and let the thread continue its processing. To achieve this, we'll use the command clear followed by the breakpoint's identifier.
The identifier is exactly the same as the one used earlier with the command stop:
> clear OurApplication:7 Removed: breakpoint OurApplication:7
To verify whether the breakpoint has correctly been removed, we'll use clear without arguments. This will display the list of existing breakpoints without the one we just deleted:
> clear Breakpoints set: breakpoint OurApplication.<init> breakpoint OurApplication.buildInstanceString(int) breakpoint OurApplication.main(java.lang.String[])
5. Conclusion
We've discovered how to use JDWP together with JDB, both JDK tools. More information on the tooling can be found in their respective documentation: JDWP's and JDB's.