Scanning Java Class Files #1: The javap Tool

Marcio EndoMarcio EndoFeb 24, 2025

In the Java programming language, Java source code is compiled into Java class files. Every class and interface defined in source code produces a Java class file. Class files are typically packaged into JAR files, which are consumed as libraries or executed as applications.

It is also possible to package and execute a Java application as a jlink runtime image. Java applications can also be ahead-of-time compiled into a native binary using GraalVM. But, before you can generate those artifacts, they require that you first compile your Java source code into Java class files.

In summary, apart from external resources required by your application, and regardless of how your application is packaged, Java class files contain all the information required to execute the Java code defined in the original Java source files.

But how is the information stored in a Java class file? Can we read it back? If so, how? And what's a practical use-case for doing so? These are the questions we'll try to answer in this blog post series.

Example

Let's create a Java class file so we can inspect its contents. We'll use the javac tool for this purpose. It requires us to write a Java source file first. So, we'll write the following Java code in a source file named HelloWorld.java:

void main() {  IO.println("Hello, World!");}

Let's use the javac tool to compile it:

$ javac --enable-preview --release 23 HelloWorld.java
Note: HelloWorld.java uses preview features of Java SE 23.
Note: Recompile with -Xlint:preview for details.

We've used JDK 23, the current version at the time of writing. The source code declares an instance main method of a implicitly declared class. Therefore, we can execute it as an application:

$ java --enable-preview HelloWorld
Hello, World!

It printed Hello World! to the console.

String Literals

A "Hello, World!" application is mundane. But it produces a minimal Java class file that does some actual work: it prints the message we've declared to the console. And, in our example, the message was declared using a string literal.

A string literal allows for representing an instance of the String class directly in source code. In our example we have:

"Hello, World!"

The "Hello World!" token represents a String object whose value is literally the one enclosed by the two double quotes. In other words, when the program is executed, a new String object is created having the exact value of the string literal. A reference to this object is then passed to the println method.

The food for thought is that, in order for the program to print the string literal value we've declared, it must have stored it somewhere in the class file. But where exactly?

Using the javap Tool

A JDK installation includes the javap tool. According to its man page, the javap tool "Disassembles one or more class files". Let's use it on our HelloWorld.class file:

$ javap -c HelloWorld.class

We've used the -c option which, according to the command help message, Disassemble the code. Here's the output:

Compiled from "HelloWorld.java"
final class HelloWorld {
  HelloWorld();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  void main();
    Code:
       0: ldc           #7                  // String Hello, World!
       2: invokestatic  #9                  // Method java/io/IO.println:(Ljava/lang/Object;)V
       5: return
}

At the bottom we find the disassembled code of our main method.

The Disassembled main Method

Let's focus on the Code section of our main method:

Code:
   0: ldc           #7                  // String Hello, World!
   2: invokestatic  #9                  // Method java/io/IO.println:(Ljava/lang/Object;)V
   5: return

We're interested in the first bytecode instruction:

0: ldc #7 // String Hello, World!

It is the ldc JVM instruction. We will not discuss the ldc instruction, it is beyond the scope of the blog post. Right now, we are interested in its argument:

  • It references the number #7.

  • From the comment, it is related to the value of our string literal "Hello, World!".

What does the number #7 refer to?

The Constant Pool

Let's run the javap tool again. This time, we'll use the -verbose option:

$ javap -verbose HelloWorld.class

The javap output now includes a section named Constant pool. It contains entries numbered from #1 to #21. Each entry has a type and a value. Here are two entries relevant to our discussion:

Constant pool:
   ...
   #7 = String             #8             // Hello, World!
   #8 = Utf8               Hello, World!
   ...

We can see that:

  • The entry #7 is of the String type and references entry #8.

  • The entry #8 is of the Utf8 type and its value is Hello, World!.

It seems we have found our string literal value.

Takeaways

Here are some takeaways from our experiment so far:

  • The javap tool can inspect the contents of a Java class file.

  • Java class files contain a section named constant pool.

  • Among others, the constant pool stores the values of string literals defined in source code.

  • Code in the method bodies references values from the constant pool.

Annotations

Let's inspect a second Java class file. We'll create it from a Java source file named Annotations.java having the following contents:

@S @C1 @C2 @R1 @R2public class Annotations {}

It is a top-level class named Annotations annotated with five different annotations. The annotations are declared in the same Java source file, after the Annotations class:

@Retention(RetentionPolicy.SOURCE)@interface S {}@Retention(RetentionPolicy.CLASS)@interface C1 {}@Retention(RetentionPolicy.CLASS)@interface C2 {}@Retention(RetentionPolicy.RUNTIME)@interface R1 {}@Retention(RetentionPolicy.RUNTIME)@interface R2 {}

We can see that the retention policy of the annotations are as follows:

  • The S annotation has SOURCE retention.

  • The C1 and C2 annotations have CLASS retention.

  • The R1 and R2 annotations have RUNTIME retention

Once again, we'll use the javac tool to compile our Annotations.java source file:

$ javac Annotations.java

Next, we'll use the javap tool to inspect the compiled class file.

Using the javap Tool

We'll run the following command:

$ javap -verbose Annotations.class

At the very bottom of the javap output we find the following:

RuntimeVisibleAnnotations:
  0: #14()
    R1
  1: #15()
    R2
RuntimeInvisibleAnnotations:
  0: #17()
    C1
  1: #18()
    C2

We have two groups of annotations:

  • Runtime Visible Annotations, containing those with RUNTIME retention.

  • Runtime Invisible Annotations, containing those with CLASS retention.

Additionally, each group references the "hashtag numbers" we came across when discussing string literals. We already know that those numbers reference entries from the constant pool table.

The Constant Pool

Earlier in the javap output we find the Constant pool section of our Annotations class. It contains entries numbered from #1 to #18. Here are the entries relevant to our discussion:

Constant pool:
  ...
  #13 = Utf8               RuntimeVisibleAnnotations
  #14 = Utf8               LR1;
  #15 = Utf8               LR2;
  #16 = Utf8               RuntimeInvisibleAnnotations
  #17 = Utf8               LC1;
  #18 = Utf8               LC2;

We can see that:

  • The shown entries are all of the Utf8 type.

  • They contain the names of the two annotation groups: RuntimeVisibleAnnotations and RuntimeInvisibleAnnotations.

  • They also contain the names of the R1, R2, C1 and C2 annotations, although the names are formatted as "L Name ;".

Takeaways

Here are some of the takeaways from our annotations experiment:

  • Annotations with RUNTIME retention are recorded in the class file. They belong to the RuntimeVisibleAnnotations group.

  • Annotations with CLASS retention are recorded in the class file. They belong to the RuntimeInvisibleAnnotations group.

  • Annotations with SOURCE retention are not recorded in the class file.

  • We're not sure where where in the class file the annotation information is recorded, but we know they reference entries from the constant pool.

In short, by scanning a Java class file, we can determine if the class is annotated with a particular annotation or not. We can do so provided the annotation itself is marked with the CLASS or the RUNTIME retention policy.

In the Next Blog Post in This Series

We've used the javap tool to inspect the class file of two distinct Java classes. From our two experiments, we saw that the constant pool plays an important role in a Java class file:

  • It stores the string literals defined in source code.

  • It stores the names of any CLASS or RUNTIME annotation applied to the class.

And, if the javap tool can inspect Java class files, we probably can do the same.

So, in the next blog post in this series, we'll begin the implementation of a class that is able to scan a Java class file.