Method invocation and control flow in JVM
Introduction to JVM and JVM languages
Java Virtual Machine (or JVM for short) is a platform-dependent software that allows you to execute programs written in languages like Java. Languages such as Scala and Kotlin utilize JVM for execution and are also often referred to as JVM languages for this reason. Code written in these languages is often identified via their file extensions such as .java and .scala. Compiling source files of these languages results in .class files, which are a special representation of your source code and contain information necessary for successful execution. Each class file begins with the magic number 0xCAFEBABE, which helps identify this format.
This is how a class file is represented as per the Java Virtual Machine Specification:
Note: The sizes are represented as values of type ux, where x is an exponent of 2. For example, u2 is a value that takes up 2 bytes or 16 bits, and u4 is 4 bytes or 32 bits. You can use javap to generate a readable representation of a class file.
The constant pool of a class is a sort of a key-value store containing entries for things like String constants, as well as references to all classes and methods that are referenced by the class. The type of each constant pool entry is indicated by a single byte falling in the integral range [1, 18], often referred to as a "constant pool tag".
Consider the following snippet:
The constant "java" is stored in the constant pool as:
You can generalize the format as:
You will also find information on classes and methods used within this class in its constant pool:
Class references (Indicated by the Class type) are composed only of one simple Utf8 entry, signifying the name of the referenced class. Method references (MethodRef entries) are more complex, and are of the form <Class>.<NameAndType>. The NameAndType entry is again composed of two Utf8 entries, i.e. the name of the method and its descriptor.
Any entry that references another entry will contain an index pointing to that other entry. For example, at index 7 is this entry: #7 = Class #8 // Foo. This entry refers to a class whose name is contained in index 8. The entry in index 8 is a Utf8 entry with the name of the class, Foo.
Any index referenced by some entry in the constant pool must be a valid index of only that constant pool.
Introduction to bytecode representation
The readable representation of the bytecode for the main method in the above example obtained via javap is:
The comments you see here are clarifications inserted by javap and do not appear in the constant pool.
Each line of a method's representation describes a single bytecode instruction in the following format:
You may have noticed that the instruction offsets shown here are discontinuous. The first instruction is at 0, while the second one starts at 3. This is because instructions may have any number of operands embedded in bytecode. For example, the invokespecial instruction requires one 2-byte operand. Similarly, the new instruction at the start takes a 2-byte operand which occupies space represented by the offsets 1 and 2, which is why 3 is the next available offset for an instruction.
Note: Bytecode is represented as a byte array and its offsets are not the same as constant pool indices.
JVM uses certain instructions such as invokevirtual, invokespecial, and invokestatic to invoke methods depending on their nature. For example, constructors are invoked via invokespecial, static methods via invokestatic, and other methods via invokevirtual. Instructions such as invokeinterface and invokedynamic fall outside this blog's scope.
Let's take a closer look at the invokevirtual instruction in the listing for main:
9: invokevirtual #10 // Method Foo.bar:()V
In the example above, invokevirtual is at offset 9. It takes one 2 byte operand, whose contents are located at offsets 10 and 11. invokevirtual's operand is interpreted as the index of a MethodRef entry in the class's constant pool. The value of the index specified is 10, meaning the tenth entry in the constant pool. javap has helpfully included the value of that entry for us as a comment — Method Foo.bar:()V. We now have all the information required for the JVM to invoke the specified method, Foo.bar(). Arguments are passed to the invoked method beforehand by pushing values onto the operand stack using instructions from the *const and *load families.
Note: Here, we say *load because this instruction can be considered to be an entire family of instructions. Depending on its prefix we can interpret it as loading an integer, a floating point constant, or even an object reference. The same principle applies to the *const family, except with only integer and floating point types (And, as a special case of a constant value, null). Examples of instructions in this family are: aload, iload, fload, etc.
if conditions, loops, and unconditional jumps are important parts of control flow. Let's take a look at how the JVM executes each of these.
Pre-requisites: Local array and stack
Every method has a small space allocated to it within the Java call stack called a frame. Frames store local variables, the operand stack for the method and also the address of the constant pool of the method's containing class.
The operand stack is, as its name indicates, a stack structure. It is used to store input and output data for instructions. For example, the iadd instruction expects two integer values to be present in the operand stack beforehand. It pops its operands from the stack, adds them, and then pushes the result back onto the operand stack for future instructions to use.
A method's parameters, and any local variables declared within it will have a predetermined slot in the corresponding stack frame's local variable array. For instance methods (non-static methods), the first entry in the local variable array will always be a reference to the object referred to by the this pointer. The referenced object and the method's declared arguments must first be pushed onto the operand stack of the calling method.
When invokevirtual is called, the number of values to pop from the operand stack is calculated based on the invoked method's descriptor. That same number of values, (plus one more for the this pointer) are popped from the operand stack. These values are then placed into the local variable array of the new frame, with the first entry always being the this pointer, followed by the arguments in their declared order.
Once the arguments are copied over, the JVM sets the program counter to the offset of the first instruction of the method and starts executing bytecode again. When the end of the method is reached, the current frame is discarded and the JVM returns control flow to the next instruction after invokevirtual. Any returned value is popped off the operand stack of the invoked method and pushed onto the operand stack of the previous method to be used by subsequent instructions.
Consider the following snippet and its bytecode:
Instructions such as ifeq, ifne, iflt, ifge, ifgt, and ifle are used when a variable (for example x in this case) is being compared against 0. These instructions pop the value off the stack, compare it against 0 and if the condition holds true, the control jumps to the specified offset. Instructions such as if_icmpxx (where xx is [eq, neq, lt, gt, ge, le]) work by popping off arguments off the stack and then comparing them.
Consider the following snippet and its bytecode:
A loop is just a set of statements executed until the specified condition evaluates to false. The bytecode generated is more or less similar to the one that we've seen previously. The only difference is that the goto instruction is used to jump to a previous offset and resume execution, i.e. to execute previously executed statements thereby essentially keeping the loop running.
JVM is one of the most exciting platforms out there. What we've seen so far in this blog is a tiny fraction of its working and internals. If you wish to further delve into JVM and its technicalities, consider getting started with The Java Virtual Machine Specification.