This chapter describes the different areas where improvements are required. Some of them need to be developed from scratch and some just need adjustment.
We waited to implement the error handling since the specification was not released until the last part of our project. We marked places where errors could occur with comments but these are not replaced with the error handling procedures. Only a few exceptions/errors are thrown at run-time to test our error handling implementation. The exceptions and errors implemented are: ArrayOutOfBoundsException, ArithmeticException (division by zero), NullPointerException and NegativeArraySizeException.
The basic strategy in error handling is as follows:
There are about 25-30 different exceptions/errors in the Java API and they are, of course, used at many different places in a complete JVM implementation.
An API (Application Programming Interface) consists of the functions and variables that programmers are allowed to use in their applications. The Java API consists of all public and protected methods of all public classes in the java.applet, java.awt, java.awt.image, java.awt.peer, java.io, java.lang, java.net and java.util packages.
We have implemented most of the java.io, java.lang and java.net packages. All of these packages contains native methods that need to be identified and implemented. The Java API is the minimum that programmers can safely assume is present in a JVM. JavaSoft will in a near future release new APIs, such as Beans, Commerce, Media and Security. To make a JVM fully compatible one will have to take on a big commitment. Axis will have to make a decision on what to support and implement the required native methods.
Native methods should be placed in a dynamic library and linked into the JVM. We have used static linking since we did not figure out how to use pointers to functions.
One important optimization is the resolution of virtual method invocation. The virtual invocation is frequently used and a lot of time is saved if it is resolved. The crux is the changes that have to be done.
Time is used to search for the virtual method in a class file. The idea with resolution is to directly find the method's code. The problem with virtual methods is that they have the same name but they do not contain the same code, i.e. they are not located in the same place in memory. If the virtual code is organized so that every virtual method has the same ``method index'', that index can be used to describe the virtual method independent of the location of the code.
The methods in the class file must be organized in a method block. To keep track on where the code starts, an array of pointers to the methods should be created. It is the index in the array that is the method index. If the virtual methods are placed first in the array it is possible to use the same index to the virtual code independent of which class it is. If this index is stored as an operand to the opcode then one extra lookup is needed compared to ordinary methods to find the code for the method. The reason to store the virtual methods first in the method block is to avoid holes in the array.
The invokevirtual opcode's operands are changed from an index into the constant_pool (CONSTANT_Methodref) to the method index of the method. Actually, it is an index into the method block of the class. In our implementation we used the methods array in the constant_pool as our method block.
It is important to use the same method block index in every class that declares the virtual method. This operation has to be done when the class file is parsed. Some indexes in the method table will not contain a method. Careful implementation has to be done to ensure that the non-existent methods aren't accessed. The parser must check the super classes to see if the virtual method is declared there and use the index of the virtual method found there.
When a method is executing it uses a stack to store temporary data. Local variables are stored in a local variable area. The size of these two areas are given in the class file and they vary depending on the method. The Java compiler computes the sizes at compile time.
In our implementation we allocate the stack and the local variable area inside each frame as it is created, i.e. when a method call has been done. Deallocation of the stack and the local variable area is done when the method is finished with its execution.
Another way to implement the local variable area and the stack is to maintain one large stack, a global stack, which is used by every frame as the stack and local variable area.
When a method is called, the arguments to the method lie on the stack in the calling method. These are popped and put as local variables in the called method's frame.
With a global stack it is possible to use the global stack both as a stack and local variable area for all the methods in one thread. The frame doesn't have to be changed.
Figure 13 shows the global stack after three method invocations. The method named A has called method B which has called C. The local variable area of the called method may actually lie over the calling method's stack. C's local variable area and stack lie entirely on B's stack.
Figure 13: The global stack with three method calls.
The advantages with a global stack are:
To improve the performance of the Scheduler a platform based thread package should be used. Platform specific advantages can be exploited e.g. interrupts.
The time checking algorithm in our implementation is introducing an unpleasant overhead to the JVM. This overhead can be removed with the use of interrupts.
A further step to improve the JVM is to include a JIT (Just-In-Time) compiler. A JIT compiles the bytecode before it is executed. This makes it possible to use machine specific code optimizations and run machine specific opcodes.
The JIT interpreter approaches purely compiled code. Although there will always be a small interpreter overhead that the compiled code doesn't have. Thorough checks are done once by the interpreter.
The changes in the interpreter is done in the method block of the class. The compiled code replaces the bytecodes. The procedure is to compile the method which will be executed and then call the method as an ordinary compiled method. This approach may be more memory consuming since both the compiled version and the original bytecodes must be in the memory at the same time. The advantage is that the program runs faster.
An existing JIT JVM is the Internet Explorer from Microsoft. It runs approximately five times faster than the interpreter from Sun.