Introduction
The Java Virtual Machine (JVM) is a piece of software that emulates the behaviour of a real CPU. Its main use is to execute the Java bytecode which is produced when a Java programme is compiled. However, the JVM has absolutely no knowledge of the syntax, semantics or even existence of the Java programming language; it merely deals with the binary class files containing Java bytecode. For that reason, any language that can be compiled into a standard format class file can be run on a machine with a JVM installed.
This writeup will give a brief overview of the internal structure and workings of the JVM, although a lot of material must be absent for the sake of conciseness. I have tried to explain any terminolgy I have used, although a background in computer science and preferably some experience of computer design would help make this more readable.
History
The idea of virtual machines is decades old, because the concept of portable interpretive code has always been attractive.
In terms of the design of the actual architecture, the JVM uses a mixture of very old and very new concepts. For example, in essence the JVM is a stack machine, which is something modern computer design is moving away from. Meanwhile, new solutions such as frames and the runtime constant pool have been found to tackle the problems raised by running object-oriented code.
Design
Types
The types in the JVM can be split into two categories: primitive and reference.
Primitves
Primitive types come in two families: the
boolean type and the
numerical types. The
boolean type can only have one of two values:
true or
false. The
numerical family encompasses a number of types:
- Integral types. These are all signed twos complement integers, apart from char.
- byte - 8 bits
- short - 16 bits
- int - 32 bits
- long - 64 bits
- char - 16 bit unsigned integer representing a Unicode character
- Floating Point types.
The behaviour out these numbers does not by default follow the strict rules set out by the IEEE standard. However, if absolute IEEE compliance is required, it can be enabled with the -strictfp command line switch (on the HotSpot JVM).
Reference
While the primitives have strict rules imposed on their size and content, a reference type is merely a pointer to an object. There are three, or maybe four, kinds of reference types: class type, interface type, array type and the null pointer.
Of these three, objects can only be created from classes and arrays. What this means, and where the objects exist is covered below in Data Storage.
Data Storage
Each thread (see below: Threads) has its own stack associated with it. A stack is a data structure that can only be manipulated at one end, i.e. it is a last in first out (LIFO) structure. The purpose of the JVM's stacks is to store frames. Each time a method is called, a new frame is created, and pushed onto the calling thread's stack, to be used throughout the execution of the method. The data stored in each frame are things like the the variables that will be used in the method and the frame's operand stack, which has a similar function to the stack in a conventional stack machine. As well as this, the frame stores the state of the invoking method, so that execution can continue cleanly after the invoked method exits, with the return value left on top of the invoking method's own operand stack.
The operand stack itself can store any JVM type, from the 8-bit byte type to the 64-bit floating point double, as well as the 8-bit instructions (opcodes) themselves.
The contents of the operand stack can actually be manipulated in a number of ways; as well as pushing values onto and popping values off the stack, the top value can be duplicated in a number of ways, or swapped with the next element down.
There is one heap that is shared between all the threads running in the JVM. The memory used to store class instances and any array is taken from this heap. The initial size and behaviour of the heap is heavily dependent on implementation; it could be of fixed or dynamic size, have the size set at runtime by the user or at compile time by the programmer.
In Java, there is no explicit freeing of memory unlike C or C++. For this reason, if no action was taken to recycle the areas of the heap no longer in use, the available space would rapidly deplete, and cause an OutOfMemoryError. However, when the JVM is running, there will always be a garbage collector thread running which traverses memory, searching for redundant objects and making that area of memory free for use once more.
However, this task is very hard to implement efficiently because of the complex graph of dependencies existing between class instances. Also, care must be taken not to garbage collect objects that are still in use. An object that satisfies any of these conditions is referred to as being reachable. It must not be garbage collected:
- a static field has a reference to it - as static objects exist from JVM startup to the JVM exiting, this guarantees reachability.
- it has not been finalized - this is when the garbage collector has marked the object for removal, but the object's cleanup operations have not yet completed.
- a local variable in a running thread refers to it - in this case, the object can be referred to through the variable at any time.
- it is referred to from any object satisfying one of these conditions - if one object is reachable, all the things it can reach must also be reachable.
It is the last possibility that causes garbage collection to be so expensive. Every reference in every reachable object must be followed, and in a large project, this could be thousands of objects to check.
There is no construct in Java nor an instruction in the JVM that explicitly tells the garbage collector to run. System.gc() gives it a hint that you would like it to run, but it is not translated into a actual "run garbage collector" instruction.
Modern garbage collectors use a variety of techniques, including ephemeral collection, the use of multiple processors or concurrent collection, so that the entire application doesn't have to stop every few seconds while the garbage collector does its work.1
Method Area
The actual code to be run must be stored somewhere, and that place is the method area. It is actually part of the heap (see above), but has quite a different function. Any data that a method requires, but that doesn't belong in the frame or heap, is put in the method area. This includes the runtime constant pool (see below), field and method data and the code for methods and constructors.
The runtime constant pool stores data on the contents of an instance of a class. This includes entries for all the methods, fields and relevant classes; it performs a task similar to that of a symbol table in conventional programming languages.
It is through the runtime constant pool that most of the non-computation work is done, e.g. invoking methods, referring to string literals and creating new instances of classes. As an example, when a "Hello World" programme is run with the HotSpot JVM, the runtime constant pool has four entries:
- #1
- Refers to the constructor of java.lang.Object, which is called when a class is instantiated.
- #2
- A reference to the static methods of java.lang.System.out which will be used in conjunction with java.io.PrintStream to actually print the string on the screen.
- #3
- Reference to the "Hello World!" string itself.
- #4
- A reference to the `println' method, which puts the string on the terminal
Instead of relying on hard links between
application components being set at
compile time, dynamic information can be placed in the pool as required.
Threads
Every time the JVM is used for some useful work, multiple threads of execution will be running inside the virtual machine. At any one time, there might be a garbage collector thread, a JIT thread, multiple user threads etc.. This abstraction may be implemented in a number of ways on the actual machine, including running separate threads on different processors in a SMP machine, time-slicing all the threads on one processor or a combination of the two.
State
As mentioned above, the JVM is a stack machine. However, each thread has four registers associated with it:
These registers are not directly accessible, so the JVM can still legitimately be referred to as a stack machine.
As well as these registers, each thread has a JVM stack which is used to execute the bytecode (see Instructions and Data Storage sections). The threads also share some resources, such as the JVM's heap, on which the objects the thread is currently working on are stored.
With multiple threads of execution running, operations could be performed on some shared resource at the same time, either through a static method or an object that multiple threads have references to. The Java language has a range of concurrency control provisions, including mutexes, semaphores and condition variables. These high level constructs are represented inside the JVM by monitors, which are manipulated with the JVM instructions monitorenter and monitorexit. Every object has a lock associated with it; if monitorenter has been called on an object, the object is "taken", and an attempt by any other thread to call monitorenter on that same object will fail, giving the original thread sole undivided access to the locked object.
Using locks brings up another problem, however. If a thread fails to release a lock it doesn't need, this can leave other threads deadlocked, unable to proceed until they acquire a lock they will never gain. The JVM has no internal structure to check for or to prevent deadlock. Instead, it relies on the fact that Java doesn't have ecplicit lock/unlock operations, unlike the bytecode. Locks are released implicitly at the end of code blocks - the programmer doesn't have to remember to call unlock operations when he has finished with a resource.
Instructions
Variable Length
Java has extensive facilities for transporting bytecode across networks, such as serialization, jar files, RMI and reflection. As part of this design ethos, the size of bytecode is deliberately kept to a minimum; this is one reason that a stack based architecture was chosen. Variable length instructions (instructions are not fixed at a certain length) also help to keep class file size to a minimum, as padding is rarely required - the bits are used more efficiently. On a standard register based machine, variable length instructions can be unbeleivably confusing and counter-productive (see x86), a much clearer and easier to use instruction set would only use fixed length instructions (see ARM). However, on a stack machine, variable length instructions are a lot more intuitive; it just means having more things pushed onto the operand stack
Using the operand stack
In a conventional stack machine, only operands and results would be stored on the stack, not the instructions themselves, which would have been fetched and decoded in some special purpose register. In the JVM, as in the conventional machine, all the operands are pushed onto the stack, but as well as this, the 8-bit instruction type (opcode) is also on the stack, above all its operands. When this opcode is popped off the stack, it reveals how many operands are left on the stack, and of what type they are. This information can be gained from the opcode because every operation has a fixed number of fixed type operands, so there are separate int and long addition instructions, for example.
To show how the stack works in a very simple case, I have compiled this application for the HotSpot JVM:
class Increment {
int doIncrement(int arg) {
return arg + 1;
}
}
This is the resultant
Java bytecode, with some commentary. The instructions are in bold:
// preamble and object initialization
int doIncrement(int);
Code:
0: iload_1
This instruction pushes the int
argument onto the stack.
Stack:
Before After
+-------+
empty | arg |
+-------+
1: iconst_1
Pushes the
constant int '1' onto the stack.
Stack:
Before After
+-------+ +-------+
| arg | | 1 |
+-------+ +-------+
| arg |
+-------+
2: iadd
Adds the two
integers on the top of the
stack,
the result is placed back on the stack.
Stack:
Before After
+-------+ +-------+
| 1 | | arg+1 |
+-------+ +-------+
| arg |
+-------+
3: ireturn
The top stack item is
popped off this frame's stack
and
pushed onto the top of the
invoking frame's stack.
The
frame is discarded as the
method exits.
Stack:
Before After
+-------+
| arg+1 | empty
+-------+
Opcodes
As instructions must be duplicated for different argument counts and types, the JVM has a lot of opcodes. As the opcode length is 8 bits, there are 28=256 different avaiable opcodes, and in fact, every single one is used, apart from opcode 186 (0xba), for "historical reasons". Of these, almost forty are integer operations, which actually probably perform no more that 10 distinct jobs, it's just that they need duplicating for different arguments.
As well as standard arithmetic instructions, there are instructions such as newarray and instanceof which perform the object-oriented tasks.
Conclusion
Implementations
At first, JVMs were only implemented in software, on computers and PDAs. Back then, the Java bytecode was only being interpreted, as described above. Nowadays, there are lots of ways that Java has been sped up, so that it outperforms C++ at some tasks!
One of these methods is the Just In Time compiler (JIT), that runs in a concurrent thread. Its job is to compile the Java bytecode into native machine code just before it is executed. This means that the JVM is bypassed for some sections of code, which are run directly on hardware, and the results passed back into the JVM.
As well as the JIT compiler, JVMs have been implemented on chips directly, either in microcode or directly in silicon. Embedded JVMs such as these have massively increased the use of Java and Java bytecode; if you want a challenge, buy a JVM chip and program your own central heating system in Java!
Porting other Languages
As well as an explosion in the use of JVMs, projects such as GNU's Kawa are attempting to increase the number of languages that can use the JVM. Already massively popular languages such as PERL will become even more so when they can be run on any Java-enabled machine. Not only does Kawa allow languages to be ported into Java bytecode very easily, it also provides a system for a wide range of languages to interact seamlessly in one application, in the same way that the proprietary Microsoft .NET does.
Although Java is a decent language, its success is built on the concept and implementation of the JVM. The idea of being able to run powerful, portable code securely across networks is the holy grail of computer science, and this is what the JVM offers. Java as a language has ridden the crest of this JVM wave, and many people will see the language and the virtual machine to be inextricably linked. However, as I have mentioned, the JVM is basically independent of the Java language, and this only makes it all the more powerful.
1The behaviour of the
garbage collector can often be modified through the use of command line switches. In the
HotSpot JVM, -verbose:gc dumps collection information to the
terminal and -Xincgc turns on incremental collection.
Sources:
The JavaTM Virtual Machine Specification, Second Edition
Tim Lindholm and Frank Yellin