Foreword
This is my personal open source implementation of the Java
Virtual machine. I created it from scratch; the programming took about three
months. The JVM is not fully implemented (some rare instructions and many native
methods are missing) but has all the necessary features (except the validation
phase of the class loading process) required by the Sun's Java Virtual Machine
Specification. It was tested on a number of classes compiled by the
standard JDK's javac compiler (test classes include, for example, exception
handling, multithreading and synchronization mechanisms).
The main goal of my implementation is to let someone who is interested in the internals of the JVM to learn the heavily commented source code and to see how the stuff works. That's why I did not implement some of the OS-related features, such as native threads using OS-threads.
My main sources of information during this implementation were the Java Virtual Machine Specification by Sun and the book "Inside the Java 2 Virtual Machine" by Bill Venners.
Design goals
- First and foremost design goal of this JVM is the clarity and readability of the code,
so that it can be easily understood.
- Another design goal is to take full control upon the execution and the memory management
of the running application. As long as possible, native threads, native pointers and system memory allocations were avoided
enabling easy control over all of these entities.
- Yet another goal was not to rely upon any existing data
structure libraries (such as STL or whatever) but implement all the necessary
data structures in a simplest possible way.
Design remarks
- To gain full control upon the execution the native threads are not used. Instead, the round-robin mechanism is implemented internally.
- Because of the internal multithreading implementation there is very clear and straightforward way to implement monitors and object locks.
The monitor is implemented in such a way that it will put the demanding thread asleep
(in case it is already acquired) by itself without even notifying the thread about this.
Symmetrically, when the thread will gain the monitor it will be put as running by the
monitor itself. Thus, for a thread it is absolutely transparent whether it has succeeded
to acquire a monitor or not. For it, the monitor is always acquired and the next instruction
is ready to be executed.
- Lazy class resolution is used, so the class is not resolved during the linkage
stage, but rather its entries are resolved upon demand. It makes the loading faster.
- References to instances are not represented by native pointers but rather mapped from the abstract word-wide handlers to the native pointers;
thus, reference does not depend on the underlying architecture.
- Each constant pool entry has a "resolved" flag indicating whether this entry is resolved
or not. If in the process of resolution of one entry some other entry turns to be not
resolved the resolution process will recursively resolve it first.
Each constant pool entry contains the direct reference to its resolved runtime counterpart;
for example CONSTANT_Class_info entry has a link to a ClassFile corresponding to this entry;
CONSTANT_Methodref_info has a link to either the method_info of the corresponding method
or the offset of the virtual method in the virtual method's table;
CONSTANT_Fieldref_info has a reference to the field_info of the corresponding class;
- String literals are "interned" by the JVM, i.e. the unique string is stored only once
within the JVM. To create such an "interned" string special technique is used:
we have to artificially initialize this String's character array without calling any constructors. It is done by proper initialization of the java.lang.String
fields (such as "value" and "count").
- There is a special heap for stack allocations used by all threads.
Each time a thread is created it allocates its own chunk for the contiguous stack.
- Stack frames are created as overlapped "operand stack"-"local variables storage"
memory block. It means that the operand stack of the stack frame of the calling method
automatically becomes the local variable storage of the stack frame of the method
being called.
This approach saves memory space and also saves time, because the execution engine does not
have to spend time copying the parameter values from one frame to another.
- Virtual methods are stored in the class' method tables consisting of the offsets of
of these methods inside the class' layout. The important point here is that such an
offset is always an invariant along the class hierarchy.
Class' method table is built during the type preparation step.
- Native method execution mechanism tries to imitate the regular Java method execution
as close as possible. Thus, the new overlapped stack frame is put on top of the Java stack
for the native method and the parameters are passed in the stack frame local variables as
for the regular Java method. Then, these parameters are pushed onto the native C-stack
and the execution goes to the native code. After it is finished, the return value is taken
from the eax register and pushed onto the Java stack frame as if it was the regular Java
method. The native's method Java-stack frame is discarded from the Java stack as usual.
-
[The following feature is not implemented] Native methods are executed within the native OS threads (otherwise their bodies
would be executed as an atomic instruction which violate fairness).
While executing the native code the thread is marked as Native and not Running so that
it cannot be chosen be the execution pool as a next candidate to execute an instruction.
Upon return the thread is marked Running again.
- Static initializers (<clinit> methods) are treated specially. Their bodies must be
executed within the body of the instruction that caused the resolution process of a new
type which, in turn, caused the static initializer of this type to be called. Thus, the instruction cannot be completed unless <clinit> is fully executed.
That's why the <clinit> method runs inside the "fake" thread with its own "tiny
execution engine" which will run uninterruptable while the <clinit> is fully done.
Then, the original instruction can continue.
- Special technique is used to "boot" the virtual machine. We want the call of
the static "main" method to be transparent for the JVM, i.e. the "main" method should be called just as any other static method. During the method
invocation the main class of the execution will be resolved just as any other class inside the JVM.
For this purpose special "bootable" class file is artificially constructed at the beginning
of the process.
[The following feature is not implemented] This class contains only one method which will push the reference to the
command line parameters "String[] args" object onto the stack and call the "main" method
of the main class using the regular "invokestatic" instruction. The last instruction of the JVM will be the "return" instruction of this "boot" method.
The "boot" method will also call the static "initializeSystemClass" method that will
initialize java.lang.System class (system properties, version and I/O streams).
- Garbage collector uses the mark-and-sweep algorithm to collect unreachable objects.
There is the performance trade-off:
to efficiently reach all the roots (i.e. the object references that are stored on all
thread's stack frames operand stacks and local variables) we maintain the table of all
such references. Thus, there is no need to traverse all the threads and all their stack
frames' operand stacks and local variables (that would be very time consuming).
However, to maintain such a table every push_reference and store_reference commands
over a stack frame must also store this reference in the global roots table and its
own local roots set.
Besides, when the frame is popped of the stack its own roots set is subtracted from
the global roots set.
The "mark" phase of the garbage collector will visit all the instance and class roots
and mark them with the current GC run counter:
1) Instance roots are all the references that are stored on all thread's stack frames -
both in local variable storage and operand stack;
For each root all the references pointed by its fields are marked recursively;
2) Class roots are the static class data stored in the class files that, in turn, are
reachable from the namespaces of all the JVM class loaders.
The "sweep" phase is working with instance and class heaps deallocating all the memory
chunks whose mark counter is less than the current GC run counter.
- Garbage collector is designed to be undistinguishable from a regular thread of execution
in terms of the execution engine. That's why it can be picked up as an execution candidate
by the engine as well as any other JVM thread.
However, most of the time GC will run with the lowest priority, and only when an
memory emergency event occur its priority boost to the SYS_PRIORITY.
-[The following feature is not implemented] Yielding mechanism is introduced to make the multithreading more fair.
Oppositely to the Java Thread's yield() function whose functionality is mainly undefined
in the JVM implementations, our internal yield call is compulsory for the JVM and is
fairly executed by all threads performing any lengthy process. The most lengthy process (of unpredictable duration) is the resolution process that can
cause a chain reaction in resolution of classes, methods and fields. That's why each time the resolution is to be performed by a thread, it calls the
"yield" function which will choose other threads to execute. The problem here is that the current thread is in the middle of its current instruction,
so when the execution engine picks up this "yielding" thread it simply returns to the
interrupted instruction without performing any "step".
Note, that since the resolution is a recursive process, the yielding can be performed
many times during one instruction execution (at the different stages of resolution).
Notes
- Note, that the line numbers in the source code are not always corresponding to the true line numbers, because some comments were altered after the HTML files were generated from C++ files.
- Only few native classes and only partially are implemented. It was not the goal of building this JVM.
- Features that are not implemented are marked with the [NOT IMPLEMENTED] or [TODO] comment in the source code.