skip to content

the jdk toolbox

javac, jar, javap, jdeps, jcmd, jdb. what's in bin/ and why.

chapter
chapter 12.01
reading time
8 min read

a JDK is not one program, it's 30 programs in $JAVA_HOME/bin. you'll touch about eight of them every week: javac, java, jar and jshell to build and run, javap and jdeps to look inside compiled code, and jcmd and jfr to poke at a JVM that's already running.

the diagnostic ones all work the same way: find the pid, attach, ask.

the picture

think of a mechanic's van. there's the stuff for building the car (javac, jar), the key that starts it (java), and a diagnostic scanner you plug into a running engine without stopping it (jcmd). most people only ever open the first drawer.

show =
buildsource → classes, docs
runstart a JVM
inspectread class files, no JVM
diagnosetalk to a live JVM
packagejars, runtimes, installers
otherkeys, serialization, rmi
jcmddiagnose · since java 7

the swiss army knife. sends commands to a live JVM: thread dumps, heap dumps, JFR, flags, GC.

jcmd · real output, jdk 25
$ jcmd 89793 help
89793:
The following commands are available:
Compiler.codecache
GC.class_histogram
GC.heap_dump
GC.heap_info
JFR.start
JFR.dump
Thread.print
VM.flags
VM.native_memory
VM.uptime
... (54 commands on jdk 25)
$ jcmd 89793 VM.uptime
11.573 s
click any tool. every transcript is real output from temurin 25.0.4.1, trimmed. try jdeprscan for a preview of your next upgrade, and jcmd for the one diagnostic tool to learn first.

ls $JAVA_HOME/bin on jdk 25 lists exactly 30. they're thin launchers: most of them start a small JVM that runs a class from a module like jdk.compiler or jdk.jcmd (that's why jps lists itself as jdk.jcmd/sun.tools.jps.Jps). so every tool here is also a java program, running on the JDK it ships with.

the build loop

you'll learn this one in a day and then never think about it again. a build tool (maven, gradle) runs exactly these steps for you.

hover a stage.

hover a stage. a build tool is mostly this loop with dependency downloads and tests bolted on.
Hello.java
public class Hello {
    public static void main(String[] args) {
        String name = args.length > 0 ? args[0] : "world";
        System.out.println("hello, " + name);
    }
}
terminal
$ javac -d out Hello.java
$ jar --create --file hello.jar --main-class Hello -C out .
$ java -jar hello.jar
hello, world
$ java Hello.java ada          # no javac step at all
hello, ada

that last line is the source launcher (java 11, and since 22 it handles programs spread over several files). it compiles in memory and runs. great for scripts and for this site, but it's still the same javac doing the work, it just never writes a .class to disk.

looking inside compiled code

two tools read class files without running anything. javap disassembles one class. jdeps maps what a whole jar depends on.

javap -c -cp out Hello
  public static void main(java.lang.String[]);
    Code:
         0: aload_0
         1: arraylength
         2: ifle          11
         5: aload_0
         6: iconst_0
         7: aaload
         8: goto          13
        11: ldc           #7     // String world
        13: astore_1
        14: getstatic     #9     // Field java/lang/System.out:Ljava/io/PrintStream;
        17: aload_1
        18: invokedynamic #15,0  // InvokeDynamic #0:makeConcatWithConstants
        23: invokevirtual #19    // Method java/io/PrintStream.println
        26: return

see line 18? "hello, " + name didn't become a StringBuilder. since java 9, javac emits one invokedynamic and lets the JVM pick the fastest way to concatenate at runtime. you can only learn that kind of thing by looking, and javap is how you look. the full instruction set is in bytecode, and the concat trick has its own page: string concatenation.

quick check
your app runs fine on your laptop but throws NoClassDefFoundError: java/sql/Connection after you ship it on a jlinked runtime. which tool would have told you beforehand?

poking a JVM that's already running

this is the drawer worth learning properly. something is slow, stuck or eating memory in prod, and you can't restart it (restarting destroys the evidence). you don't need an agent or a special flag: every HotSpot JVM accepts commands from jcmd out of the box.

process 1 · your terminal$ jcmd 4242 Thread.printprocess 2 · JVM, pid 4242mainGC threadsJIT compilersAttach Listener.attach_pid4242SIGQUIT/tmp/.java_pid4242"Thread.print" → · ← the dump, as text

you run jcmd 4242 Thread.print. jcmd and the JVM are two separate processes. nothing is listening yet.

step 1/5
step through it. the second time you attach, the socket already exists and jcmd skips straight to the last step. on macOS the socket lives in $TMPDIR instead of /tmp; it showed up there as srw------- while i attached.

so the whole workflow is two commands: jps (or jcmd with no arguments) to get the pid, then jcmd <pid> <command>. jcmd <pid> help lists 54 commands on jdk 25. you need about six:

the questionthe command
which JVMs are running?jcmd (or jps -l)
what is every thread doing right now?jcmd <pid> Thread.print
what's filling the heap?jcmd <pid> GC.class_histogram
give me the whole heap to analyzejcmd <pid> GC.heap_dump /tmp/heap.hprof
what flags did it actually start with?jcmd <pid> VM.flags
record everything for a minutejcmd <pid> JFR.start duration=60s filename=app.jfr

jstack, jmap and jinfo do some of the same jobs. they're older (java 5) and still work, but jcmd (java 7) is the one tool that does all of it, so that's the one to remember.

and a thread dump is where a deadlock confesses. two threads, two locks, opposite order:

jcmd 12615 Thread.print (the end of it)
Found one Java-level deadlock:
=============================
"alice":
  waiting to lock monitor 0x00000075e149e3e0 (object 0x0000007018418118, a java.lang.Object),
  which is held by "bob"
 
"bob":
  waiting to lock monitor 0x00000075e149e4c0 (object 0x0000007018418108, a java.lang.Object),
  which is held by "alice"

the JVM found the cycle itself. you didn't have to read 30 stacks to spot it. why that happens and how to avoid it is in deadlocks.

the rest of the box

  • jshell (java 9) is a REPL. it's the fastest way to check what "a,b,,".split(",") actually returns. jshell has the tour.
  • jwebserver (java 18) serves a folder over http in one command. for testing, docs and teaching. it's not hardened for production and says so.
  • jlink and jpackage build a runtime with only the modules you need, then wrap it into a native installer. that's the next-but-one chapter.
  • jfr reads flight recordings. flight recorder is all about it.
  • keytool and jarsigner handle keys, certificates and signed jars. you'll meet keytool the first time a TLS handshake fails because a corporate CA isn't in cacerts.
one level deeper:why are there still tools for RMI and serialVersionUID?+

because the JDK almost never deletes a tool without a deprecation cycle first. rmiregistry and serialver date from java 1.1 and still have users. the ones that did go away went loudly: appletviewer (gone in 11), jjs (the nashorn JavaScript shell, gone in 15), and jrunscript, which was deprecated for removal in 24 and deleted in 26. run it on 25 and it warns you first. jdeprscan gives your own code the same courtesy.

keylines
  • the JDK is 30 small programs. learn javac, java, jar, jshell, javap, jdeps, jcmd and jfr.
  • javac --release N is how you safely target an older java.
  • javap -c shows what javac actually wrote. trust it over blog posts.
  • a live JVM accepts commands: jcmd <pid> help. no restart, no agent, no setup.
  • thread dumps catch deadlocks for you. take a few, a few seconds apart.