Creating a Javap Tool

Naveen AnamaneniUpdated · 7 min read

In this lesson, you will build a small version of javap: a program that opens a compiled class and prints its version, fields, methods and the bytecode instructions of each method. Doing it yourself shows that a .class file is just structured bytes with a documented layout, and it introduces the Class-File API (java.lang.classfile), the JDK's standard library for reading and writing class files, which became final in Java 24 (JEP 484). This is an optional, advanced lesson that ties together "Javap Tool in Java" and "Bytecode Basics in Java".

The Layout of a Class File

Every class file has the same parts, in this order:

PartSizeContents
Magic number4 bytesAlways CA FE BA BE
Minor and major version2 + 2 bytesFor example 0 and 69 (Java 25)
Constant poolVariableEvery name, descriptor, string and number used by the class
Access flags2 bytespublic, final, interface, abstract, ... as bits
This class, super class2 + 2 bytesConstant pool indexes
InterfacesVariableConstant pool indexes
FieldsVariableFlags, name, descriptor and attributes for each field
MethodsVariableFlags, name, descriptor and attributes; the Code attribute holds the bytecode
AttributesVariableExtras such as the source file name and record components

The first eight bytes are easy to read by hand with a DataInputStream. Everything after that depends on parsing the constant pool, which has more than a dozen entry kinds. That is the work the Class-File API does for you.

The Class-File API

TypeDescription
ClassFile.of().parse(bytes)It is used to parse a class file into a ClassModel.
ClassModelThe whole class: thisClass(), superclass(), flags(), fields(), methods().
FieldModelOne field: fieldName(), fieldType(), flags().
MethodModelOne method: methodName(), methodType(), flags(), code().
CodeModelA method's body; iterating it yields CodeElements.
InstructionA code element that is a bytecode instruction, with an opcode().

Instructions come in kinds such as LoadInstruction, FieldInstruction, InvokeInstruction and ConstantInstruction. They are a sealed hierarchy, so a pattern-matching switch is the natural way to handle them.

The Program

MiniJavap.java▶ Try it
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.InputStream;
import java.lang.classfile.ClassFile;
import java.lang.classfile.ClassModel;
import java.lang.classfile.CodeElement;
import java.lang.classfile.FieldModel;
import java.lang.classfile.Instruction;
import java.lang.classfile.MethodModel;
import java.lang.classfile.instruction.ConstantInstruction;
import java.lang.classfile.instruction.FieldInstruction;
import java.lang.classfile.instruction.InvokeInstruction;
import java.lang.classfile.instruction.LoadInstruction;
 
public class MiniJavap {
    static class Counter {
        private int count;
 
        int next(int step) {
            count += step;
            return count;
        }
 
        static boolean isEven(int n) {
            return n % 2 == 0;
        }
    }
 
    static byte[] bytesOf(Class<?> c) throws Exception {
        String file = c.getName().replace('.', '/') + ".class";
        try (InputStream in = c.getClassLoader().getResourceAsStream(file)) {
            return in.readAllBytes();
        }
    }
 
    static void header(byte[] bytes) throws Exception {
        DataInputStream in =
            new DataInputStream(new ByteArrayInputStream(bytes));
        int magic = in.readInt();
        int minor = in.readUnsignedShort();
        int major = in.readUnsignedShort();
        System.out.printf("magic %X, version %d.%d (Java %d)%n",
            magic, major, minor, major - 44);
    }
 
    static String describe(Instruction i) {
        String name = i.opcode().name().toLowerCase();
        return switch (i) {
            case FieldInstruction f -> name + " "
                + f.name().stringValue() + ":" + f.type().stringValue();
            case InvokeInstruction v -> name + " "
                + v.owner().asInternalName() + "."
                + v.name().stringValue() + v.type().stringValue();
            case LoadInstruction l -> name + " (slot " + l.slot() + ")";
            case ConstantInstruction c -> name + " ("
                + c.constantValue() + ")";
            default -> name;
        };
    }
 
    static void print(byte[] bytes) {
        ClassModel cm = ClassFile.of().parse(bytes);
        System.out.println("class " + cm.thisClass().asInternalName()
            + " extends " + cm.superclass().get().asInternalName());
        for (FieldModel f : cm.fields()) {
            System.out.println(f.flags().flags() + " field "
                + f.fieldName().stringValue() + " "
                + f.fieldType().stringValue());
        }
        for (MethodModel m : cm.methods()) {
            System.out.println(m.flags().flags() + " method "
                + m.methodName().stringValue()
                + m.methodType().stringValue());
            m.code().ifPresent(code -> {
                for (CodeElement e : code) {
                    if (e instanceof Instruction i) {
                        System.out.println("    " + describe(i));
                    }
                }
            });
        }
    }
 
    public static void main(String[] args) throws Exception {
        byte[] bytes = bytesOf(Counter.class);
        header(bytes);
        print(bytes);
    }
}
Output
magic CAFEBABE, version 69.0 (Java 25)
class MiniJavap$Counter extends java/lang/Object
[PRIVATE] field count I
[] method <init>()V
    aload_0 (slot 0)
    invokespecial java/lang/Object.<init>()V
    return
[] method next(I)I
    aload_0 (slot 0)
    dup
    getfield count:I
    iload_1 (slot 1)
    iadd
    putfield count:I
    aload_0 (slot 0)
    getfield count:I
    ireturn
[STATIC] method isEven(I)Z
    iload_0 (slot 0)
    iconst_2 (2)
    irem
    ifne
    iconst_1 (1)
    goto
    iconst_0 (0)
    ireturn

How It Works

  1. bytesOf finds the class's own .class data through its class loader. A class named a.b.C is stored as the resource a/b/C.class.
  2. header reads the first eight bytes by hand: the int magic number prints as CAFEBABE, followed by two unsigned 16-bit numbers, the minor and major version. The major version minus 44 is the Java release, here 25.
  3. ClassFile.of().parse(bytes) turns the bytes into a ClassModel. Names come back in the JVM's internal form with slashes, and types as descriptors: I is int, (I)Z is "takes an int, returns a boolean".
  4. Fields and methods are listed with their flags. An empty flag set, [], means package-private with no other modifiers.
  5. m.code() is an Optional, because abstract and native methods have no code. Iterating a CodeModel yields instructions and other elements such as labels and line numbers, so the loop keeps only Instructions.
  6. describe uses a pattern-matching switch on the kind of instruction to print its details: the field for getfield, the target for invokespecial, the slot for loads, the value for constants.

Reading the output: the compiler generated the constructor <init>, which only calls Object's constructor. In next, count += step became aload_0, dup, getfield, iload_1, iadd, putfield: dup copies the this reference so one copy is used to read the field and the other to write it. In isEven, n % 2 == 0 became irem followed by ifne, a jump taken when the remainder is not zero.

What Real Tools Add

The real javap also prints byte positions, jump targets, the constant pool, and attributes such as LineNumberTable. The same API can write class files too: ClassFile.of().build(...) generates a class, and transformClass(...) rewrites an existing one. Frameworks use these abilities to create proxies, add logging, and measure code coverage. Before Java 24, tools depended on third-party libraries such as ASM and Byte Buddy, which had to be updated for every new class file version; a standard API inside the JDK always matches the current format.

Common Mistakes

Trying to parse the whole file by hand. The constant pool makes it tedious and error-prone; use the Class-File API.

Expecting every method to have code. Abstract and native methods don't; check the Optional.

Treating every CodeElement as an instruction. Labels, line numbers and local variable entries are code elements too.

Using the API on older Java versions. java.lang.classfile is standard from Java 24; before that, use ASM.

Practice

  1. Easy: Change main so it prints java.lang.Runnable (use ClassLoader.getSystemResourceAsStream("java/lang/Runnable.class")) and observe a method without code.
  2. Harder: Extend describe so that branch instructions (BranchInstruction) are marked with the word "jump", and print each method's maximum stack size and local count from the CodeAttribute.

Quick check: Building javap

4 questions
1What are the first four bytes of every class file?
2Which Java version produces major version 69?
3Why does MethodModel.code() return an Optional?
4What is the Class-File API?

Key Takeaways

Was this tutorial helpful?

Lesson 474 of 482