Scanning Java Class Files #1: The javap Tool

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 isHello, 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 hasSOURCE
retention. -
The
C1
andC2
annotations haveCLASS
retention. -
The
R1
andR2
annotations haveRUNTIME
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
andC2
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 theRuntimeVisibleAnnotations
group. -
Annotations with
CLASS
retention are recorded in the class file. They belong to theRuntimeInvisibleAnnotations
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
orRUNTIME
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.