Gunnar Morling

Gunnar Morling

Random Musings on All Things Software Engineering

Gunnar Morling

Gunnar Morling

Random Musings on All Things Software Engineering

Building hsdis for OpenJDK 15

Posted at Oct 5, 2020

Lately I’ve been fascinated by the possibility to analyse the assembly code emitted by the Java JIT (just-in-time) compiler. So far I had only looked only into Java class files using javap; diving into the world of assembly code feels a bit like Alice must have felt when falling down the rabbit whole into wonderland.

My motivation for this exploration was trying to understand what is faster in Java: a switch statement over strings, or a lookup in a hash map. Solely looking at Java bytecode isn’t going far enough to answer this question, as the difference lies in the actual assembly statements executed on the CPU. I’ll keep the details around that for another time; in this post I’m just going quickly to share what I learned in regards to building a tool needed for this exercise, hsdis.

hsdis is a disassembler library which can be used with the java runtime as well as tools such as JitWatch to analyse the code produced by the Java JIT compiler. For licensing reasons though it doesn’t come as a binary with the JDK. Instead, you need it to build yourself from source. Instructions for doing so are spread across a few different places, but I couldn’t find any 100% current information, in particular as OpenJDK has moved to git and GitHub just recently.

So here is what you need to do in order to build hsdis for OpenJDK 15; in my case I’m running on macOS, slightly different steps may apply for other platforms. First, get the OpenJDK source code and check out the version for which you want to build hsdis:

git clone
git checkout jdk-15+36 # Current stable JDK 15 build

The source location of hsdis has changed with the move from Mercurial to git:

cd src/utils/hsdis

In order to build hsdis, you’ll need the GNU Binutils, a collection of several binary tools:

tar xvf binutils-2.35.tar.gz

Then run the actual hsdis build (macOS comes with all the required tools like make):

make BINUTILS=binutils-2.35 ARCH=amd64

This will take a few minutes; if all goes well, there’ll be hsdis binary in the build directory, in my case this is build/macosx-amd64/hsdis-amd64.dylib. Copy the library to lib/server of our JDK:

sudo cp build/macosx-amd64/hsdis-amd64.dylib $JAVA_HOME/lib/server

If you’re on Linux, you also can provide the hsdis tool via the LD_LIBRARY_PATH environment variable:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:path/to/hsdis/build/linux-amd64

Note this won’t work on current macOS versions unfortunately due to its System Integrity Protection feature (SIP). Thanks to Brice Dutheil for this tip!

Congrats! You now can use the XX:+PrintAssembly flag of the java command to examine the assembly code of your Java program. Let’s give it a try. Create a Java source file with the following contents:

public class PrintAssemblyTest {

  public static void main(String... args) {
    PrintAssemblyTest hello = new PrintAssemblyTest();
    for(int i = 0; i <= 10_000_000; i++) {

  private void hello(int i) {
    if (i % 1_000_000 == 0) {
      System.out.println("Hello, " + i);

Compile and run it like so:

java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly \
 -Xlog:class+load=info -XX:+LogCompilation \

You should then find the assembly code of the hello() method somewhere in the output:

============================= C2-compiled nmethod ==============================
----------------------------------- Assembly -----------------------------------

Compiled method (c2)    1409  106       4       PrintAssemblyTest::hello (20 bytes)
 total in heap  [0x000000011e3fce90,0x000000011e3fd148] = 696
 relocation     [0x000000011e3fcfe8,0x000000011e3fcff8] = 16
 main code      [0x000000011e3fd000,0x000000011e3fd080] = 128
 stub code      [0x000000011e3fd080,0x000000011e3fd098] = 24
 oops           [0x000000011e3fd098,0x000000011e3fd0a0] = 8
 metadata       [0x000000011e3fd0a0,0x000000011e3fd0a8] = 8
 scopes data    [0x000000011e3fd0a8,0x000000011e3fd0d0] = 40
 scopes pcs     [0x000000011e3fd0d0,0x000000011e3fd140] = 112
 dependencies   [0x000000011e3fd140,0x000000011e3fd148] = 8

[Constant Pool (empty)]


[Entry Point]
  # {method} {0x000000010d74c4b0} 'hello' '(I)V' in 'PrintAssemblyTest'
  # this:     rsi:rsi   = 'PrintAssemblyTest'
  # parm0:    rdx       = int
  #           [sp+0x30]  (sp of caller)
  0x000000011e3fd000:   mov    0x8(%rsi),%r10d
  0x000000011e3fd004:   shl    $0x3,%r10
  0x000000011e3fd008:   movabs $0x800000000,%r11
  0x000000011e3fd012:   add    %r11,%r10
  0x000000011e3fd015:   cmp    %r10,%rax
  0x000000011e3fd018:   jne    0x0000000116977100           ;   {runtime_call ic_miss_stub}
  0x000000011e3fd01e:   xchg   %ax,%ax
[Verified Entry Point]
  0x000000011e3fd020:   mov    %eax,-0x14000(%rsp)
  0x000000011e3fd027:   push   %rbp
  0x000000011e3fd028:   sub    $0x20,%rsp                   ;*synchronization entry
                                                            ; - PrintAssemblyTest::hello@-1 (line 10)
  0x000000011e3fd02c:   movslq %edx,%r10
  0x000000011e3fd02f:   mov    %edx,%r11d
  0x000000011e3fd032:   sar    $0x1f,%r11d
  0x000000011e3fd036:   imul   $0x431bde83,%r10,%r10
  0x000000011e3fd03d:   sar    $0x32,%r10
  0x000000011e3fd041:   mov    %r10d,%r10d
  0x000000011e3fd044:   sub    %r11d,%r10d
  0x000000011e3fd047:   imul   $0xf4240,%r10d,%r10d         ;*irem {reexecute=0 rethrow=0 return_oop=0}
                                                            ; - PrintAssemblyTest::hello@3 (line 10)
  0x000000011e3fd04e:   cmp    %r10d,%edx
  0x000000011e3fd051:   je     0x000000011e3fd063           ;*ifne {reexecute=0 rethrow=0 return_oop=0}
                                                            ; - PrintAssemblyTest::hello@4 (line 10)
  0x000000011e3fd053:   add    $0x20,%rsp
  0x000000011e3fd057:   pop    %rbp
  0x000000011e3fd058:   mov    0x110(%r15),%r10
  0x000000011e3fd05f:   test   %eax,(%r10)                  ;   {poll_return}
  0x000000011e3fd062:   retq
  0x000000011e3fd063:   mov    %edx,%ebp
  0x000000011e3fd065:   sub    %r10d,%ebp                   ;*irem {reexecute=0 rethrow=0 return_oop=0}
                                                            ; - PrintAssemblyTest::hello@3 (line 10)
  0x000000011e3fd068:   mov    $0xffffff45,%esi
  0x000000011e3fd06d:   mov    %edx,(%rsp)
  0x000000011e3fd070:   data16 xchg %ax,%ax
  0x000000011e3fd073:   callq  0x0000000116979080           ; ImmutableOopMap {}
                                                            ;*ifne {reexecute=1 rethrow=0 return_oop=0}
                                                            ; - (reexecute) PrintAssemblyTest::hello@4 (line 10)
                                                            ;   {runtime_call UncommonTrapBlob}
  0x000000011e3fd078:   hlt
  0x000000011e3fd079:   hlt
  0x000000011e3fd07a:   hlt
  0x000000011e3fd07b:   hlt
  0x000000011e3fd07c:   hlt
  0x000000011e3fd07d:   hlt
  0x000000011e3fd07e:   hlt
  0x000000011e3fd07f:   hlt
[Exception Handler]
  0x000000011e3fd080:   jmpq   0x0000000116a22d80           ;   {no_reloc}
[Deopt Handler Code]
  0x000000011e3fd085:   callq  0x000000011e3fd08a
  0x000000011e3fd08a:   subq   $0x5,(%rsp)
  0x000000011e3fd08f:   jmpq   0x0000000116978ca0           ;   {runtime_call DeoptimizationBlob}
  0x000000011e3fd094:   hlt
  0x000000011e3fd095:   hlt
  0x000000011e3fd096:   hlt
  0x000000011e3fd097:   hlt

Interpreting the output is left as an exercise for the astute reader ;-) A great resource for getting started doing so is the post PrintAssembly output explained! by Jean-Philippe Bempel.

With hsdis in place, you also can use the excellent JitWatch tool for analysing the assembly code, which e.g. not only provides an easy way to navigate from source code to byte code to assembly code, but also comes with helpful tooltips explaining the meaning of the different assembly mnemonics.