Newer specification draft (version 1.1.15) -- not completed.
Kouya Shimura
Fujitsu Laboratories
kouya@flab.fujitsu.co.jp
Translation by Satoshi Matsuoka
Tokyo Institute of Technology
matsu@is.titech.ac.jp
Friday, October 29, 1999
This internal specification covers the runtime portion of the OpenJIT backend compiler for SPARC V8 CPU. We outline the structure of this specification document below. The OpenJIT Backend compiler largely consists of the compiler part and the runtime part; this document covers the runtime part.
The Java JDK has several APIs for JIT compilers. OpenJIT is plugged into a given JDK using this API. We first explain the API, and describe the implementation in OpenJIT.
When JDK starts up, it first reads the Java system class (java.lang.System), and then loads the java.lang.Compiler class. Java.lang.Compiler is defined as follows:
public final class Compiler {
private Compiler() {} // don't make instances
private static native void initialize();
static {
try {
String library = System.getProperty("java.compiler");
if (library != null) {
System.loadLibrary(library);
initialize();
}
} catch (Throwable e) {
}
}
...
}
When this class is loaded, the class initializer is executed, and looks for the property "java.compiler". On JDK 1.1.x, the user either specifies the compiler via a command-line option "-Djava.compiler=XXX", or sets the environment variable JAVA_COMPILER, allowing the system to dynamically link the library via System.loadLibrary. Then, the native method initialize() is invoked. This method is defined in C as follows:
void java_lang_Compiler_initialize (Hjava_lang_Compiler *this)
{
void *address = (void *)sysDynamicLink("java_lang_Compiler_start");
if (address != 0) {
(*(void (*) (void **)) address) (CompiledCodeLinkVector);
}
compilerInitialized = TRUE;
}
By defining the function java_lang_Compiler_start() in the OpenJIT native library, this function is thereby invoked by the JVM, allowing proper initialization of OpenJIT. The argument CompiledCodeLinkVector passes the necessary values for JIT compilation; it essentially is a vector of hook functions for JIT compilation, and by modifying the vector appropriately, the JIT compiler is invoked appropriately by the JDK when needed. Some essential hook functions are described in Table 1.
| Function name | Feature |
|---|---|
InitializeForCompiler |
On class load |
invokeCompiledMethod |
On method invocation |
CompiledCodeSignalHandler |
On signal occurrence |
CompilerFreeClass |
When class is no longer needed |
CompilerCompileClass |
Compilation of specified class |
CompilercompileClasses |
Compilation of specified classes |
CompilerEnable |
Enable Compiler |
CompilerDisable |
Disable Compiler |
ReadInCompiledCode |
Load pre-compiled code |
PCinCompiledCode |
For exception handling |
CompiledCodePC |
For exception handling |
CompiledFramePrev |
For exception handling |

Figure 1 illustrates the overview of JIT compilation process. When Java is invoked with a JIT compiler specified, the native portion of the JDK is dynamically linked into the JVM. By setting the hook functions appropriately as described above, the class loader will always call the InitializeForCompiler() function on each class load.
InitializeForCompiler, in turn, modifies the invoker of all the methods defined for the class (except for the native and abstract methods) so that the JIT compiler is invoked for dynamic compilation from the bytecode to the native code. When the method is compiled, the method invocation flag is modified so that the compiled code is now executed directly, and passes on the control to the generated native code. Thereafter, JVM invokes the compiled native code directly by the virtue of the flag.
When a compiled native code calls another compiled native code, the CompiledCode field of the methodblock structure is read and the control is transferred directly via a jump instruction.
The compiled native method accumulates in memory. The space is reclaimed when the hook function CompilerFreeClass is called when the JDK actually deletes the class from memory. The hook function in turn frees the memory for the native code as well.
We have overviewed the JIT compilation process. As we can see, the unit of dynamic compilation is each individual method, and compilation happens on the first time the method is invoked. By all mans one can have adaptive strategies to compile on nth invocation, etc.
java_lang_Compiler_start)In OpenJIT, the initialization routine java_lang_Compiler_start performs the following task:
_compile_lock), which is initialized here.org.OpenJIT.Sparc class
ClassClass structure), the OpenJIT classes are loaded. OpenJIT instantiates the OpenJIT compiler classes before it starts compilation of a Java method; the pointer is required for this (*** 意味がいまいち)org.OpenJIT.ExceptionHandler class
org.OpenJIT.ExceptionHandler class by loading it.OpenJIT_compile() function
ResolveClassConstant() function, obtaining the methodblock structure for OpenJIT_compile().org.OpenJIT.Compile class
CompiledCodeLinkVector
CompiledCodeVector. Instead, we must re-initialize the classes already loaded by resetting the method invocation functions etc. of the already loaded classes, in order to allow them to be compiled as well.OpenJIT_InitializeForCompiler)static void OpenJIT_InitializeForCompiler(ClassClass *cb)
Given a class, set the invoker functions of all the methods except for the native methods and the abstract methods so that they are dynamically compiled on invocation. Also, in order to allow calls to a compiled method from another compiled method, the compilation must set the CompiledCode field of the methodblock structure.
Also, we set the CompiledCodeFlags of the methodblock structure depending on the type of the return value of the method. This value is used in dispatchVM() function when the compiled native calls the JVM for interpretive execution.
Furthermore, by setting a command-line option, we can restrict the classes and methods to be compiled by calling the function match_compile_methods. We pass the methodblock structure to the function, and if the return value is TRUE then the method is subject to compilation, whereas if FALSE then the method is not to be compiled.
OpenJIT_SignalHandler)static void OpenJIT_SignalHandler(int sig, siginfo_t *info, ucontext_t *uc)
Called when a signal occurs. OpenJIT generates Java exceptions with Unix signals. This function looks at the signal for Java exceptions, and routes control to an appropriate handler. If it receives a signal that is irrelevant to Java exceptions, it simply returns. For details of the exception handling please refer to Section 5.
OpenJIT_CompilerFreeClass)static void OpenJIT_CompilerFreeClass(ClassClass *cb)
When JDK decides that a certain class is no longer necessary, this function is called, freeing the space occupied by the compiled native code.
OpenJIT_CompilerCompileClass)static bool_t OpenJIT_CompilerCompileClass(ClassClass *cb)
Called from a java user program with the following: java.lang.Compiler.compileClass(clazz). This forces compilation of all methods that have not been compiled in the given class.
OpenJIT_CompilerEnable)static void OpenJIT_CompilerEnable()
Called from a java user program with the following: java.lang.Compiler.enable(). The methods that are called following this call will be compiled.
OpenJIT_CompilerDisable)static void OpenJIT_CompilerDisable()
Called from a java user program with the following: java.lang.Compiler.disable(). The subsequent methods called after this call will not be compiled. Note that, for the default OpenJIT implementation where each method is compiled on its first invocation, the caller of this method will have been already compiled and thus will execute as compiled native code.
OpenJIT_PCinCompiledCode)static bool_t OpenJIT_PCinCompiledCode(caddr_t *pc, struct methodblock *mb)
Judges whether the current execution is within a given method using the program counter and the methodblock structure. This function is used by the JDK when an exception occurs and it traces and displays the stacktrace of execution. If the method is being executed, then it returns TRUE, otherwise FALSE.
OpenJIT_CompiledCodePC)static unsigned char *OpenJIT_CompiledCodePC(JavaFrame *frame, struct methodblock *mb)
Returns the value of the program counter given the frame and the methodblock. This function is used by the JDK when an exception occurs and it traces and displays the stacktrace of execution.
methodblock. Everything seems to work fine under this simplification for JDK 1.1.x and JDK 1.2, but other JDKs might break this assumption.
OpenJIT_CompiledFramePrev)static JavaFrame *OpenJIT_CompiledFramePrev(JavaFrame *frame, JavaFrame *buf)
Converts the native compiled code stackframe into Java stackframe used by the JDK (JavaFrame). This function is used by the JDK when an exception occurs and it traces and displays the stacktrace of execution.
The generated code by the OpenJIT follows the C stackframe convention, and this function performs the conversion under that assumption. For JDK 1.1.x, the JavaFrame structure only utilizes the current method and the vars frame; thus, in practice these are the only two fields set by the function. As the converted frame must use the buf memory region, the function sets the values and returns buf.
For each method, both JIT compilation and and transfer of control to the native method happens at the point of the subject method invocation.
The JVM interpreter loop is structured as follows. When a method is invoked, the invoker function of the methodblock structure mb is called. Under interpretive execution, this in turn calls the JVM to generate a new Java stack frame. The first argument of o invoker() is a pointer to a class object for static method calls, and is the pointer to the invoked object on normal method calls. The second argument mb is a pointer to the methoblock structure, and the third argument args_size indicates the types of the arguments. The 4th argument ee is a pointer to the execution environment structure ExecEnv.
while(1) {
get opcode from pc
switch(opcode) {
...(various implementation of the JVM bytecodes)
callmethod:
mb->invoker(o, mb, args_size, ee);
frame = ee->current_frame; /* setup java frame */
pc = frame->lastpc; /* setup pc*/
break;
}
}
As mentioned earlier, we substitute the value of the invoker to OpenJIT_invoke when the class is loader. The OpenJIT_invoke function is defined as follows in C:
bool_t OpenJIT_invoke(JHandle *o, struct methodblock *mb,
int args_size, ExecEnv *ee);
This function in turns upcalls the OpenJIT_compile to dynamically compile the method. Thereafter, the control is transferred to mb->invoker, transferring control to the just compiled method.
A method is compiled and the invoker as well as the CompiledCode fields of the methodblock structure are initialized.
void OpenJIT_compile(struct methodblock *mb)
This function performs the followings:
CompiledCode fields in the methodblock structure. When a method is invoked during compilation, we set the invoker and CompiledCode so that the interpreter is invoked. This allows natural handling of recursive self-compilation of OpenJIT compiler classes.OpenJIT_Sparc_compile()). The Java method is upcalled to perform the actual compilation.methodblock structure so that the compiled native code is invoked.methodblock field values are restored to their original values.The CompileMethod function sets the value of the invoker to one of the following stub functions according to the return type of the method. The last letter of the function indicates the return type: V:void, I:int, J:long, F:float, D:double. Other types such as Object or short,char,byte that could be encoded in 1 word use I as a default.
bool_t invokeCompiledCodeV(JHandle *o, struct methodblock *mb,
int args_size, ExecEnv *ee)
bool_t invokeCompiledCodeI(JHandle *o, struct methodblock *mb,
int args_size, ExecEnv *ee)
bool_t invokeCompiledCodeJ(JHandle *o, struct methodblock *mb,
int args_size, ExecEnv *ee)
bool_t invokeCompiledCodeF(JHandle *o, struct methodblock *mb,
int args_size, ExecEnv *ee)
bool_t invokeCompiledCodeD(JHandle *o, struct methodblock *mb,
int args_size, ExecEnv *ee)
These stub function performs the following:
makeCompiledFrame. A dummy JVM frame is created for exception handling and Java reflection, etc.invokeCompiledCode is four arguments; should there be more arguments in the call, then the save area for the arguments must be allocated on the stack by bumping the %sp.methodblock structure in register %3: Required for compiled code calling convention. For details, refer to the descriptions of the compiled code to compiled code details.CompiledCode.
Steps 2~4 are already packaged in the Macro INVOKE_COMPILED_CODE.
As mentioned earlier, control is passed to the CompliedCode field of the methodblock. The C-style description of the call is as follows:
mb->CompiledCode(obj, arg0, arg1, arg2, ...)
The arguments are passed via SPARC function call convention, i.e., the first 6 arguments are passed in the registers %o0 ~ %o5. Note that, for Java native code, %o is reserved for the object or the class pointer of the call, so the register usage actually is shifted by one. As an example for the following Java program:
obj.method(arg0, arg1, arg2, arg3, arg4, arg5)
%o0 will have obj, %o1 will be assigned arg0, ..., %o5 will be assigned arg4, whereas arg5 is allocated onto the stack as caller-save register.
Similarly, the method return values also follow the SPARC function call convention, i.e., for integers, the value is returned in %i0, for longs (64-bits) %0 and %1, float in %f0, and doubles are returned in %f0 and %f1.
What differs from C function calls is that, we must always set the methodblock of the method to be called into the %g3 register. This value is necessary for virtual function calls (OpenJIT_invokevirtual, OpenJIT_invokevirtualobject_quick), as well as for exception handling, upon which the value must be saved in the native stack.
The methods to be compiled by the JITs already have the value of the CompiledCode field in the methodblock changed to dispatchJITCompiler on class loading time. When this method is invoked for the first time from within the compiled code, the dispatchJITCompiler is invoked. (***) This function, if written in C, would have the following interface:
? dispatchJITCompiler(? arg0, ? arg1, ? arg2, ...)
The types of the arguments and the return value depends on the method, and is indeterminate at compile time. In practice, we use assembly for efficiency to code in the following way:
dispatchJITCompiler: save %sp,-112,%sp call compileMethod,0 ! compileMethod(mb) mov %g3,%o0 ld [%g3+.off_CompiledCode],%l1 ! jump to mb->CompiledCode jmp %l1 restore
Firstly, compileMethod is invoked. The compileMethod does the compilation and sets the CompiledCode field. Then, this field is re-invoked with the same arguments, effectively executing the compiled native code.
When the CompiledCode field of the methodblock structure is set to be dispatchJVM, then the following function is called to transfer the control to the interpreter. This is used when compilation fails, or the compilation is restricted due to the compiler option.
void dispatchJVM(? arg0, ? arg1, ? arg2, ...)
JVM facilitates the following function to call a Java method from native code in general:
long do_execute_java_method(ExecEnv *ee, void *obj, char *method_name,
char *signature, struct methodblock *mb,
bool_t isStaticCall, ...);
DispatchJVM interfaces the calling convention of the compiled native methods and this function:
methodblock structure from %g3methodblock structure of the caller into the dummy Java frame. This is required for reflection and exception handling.do_execute_java_methodWhen the method is originally native to begin with, the code field of the methodblock points to the native code to be executed. A call can be thus made in the following way in C:
optop = (*(stack_item *(*)(stack_item*, ExecEnv*))mb->code)(optop, ee)
The first argument is a pointer to the operand stack, and the second argument is a pointer to the execution environment ExecEnv. As a result, when we call a native method from the compiled native code, we must assign the register arguments into operand stack similarly to the call to the JVM. Also, the returned value on the operand stack must be placed back in the register. For this purpose, we define 5 stub functions according to the return type:
void dispatchNormNativeV(...) int dispatchNormNativeI(...) int64_t dispatchNormNativeJ(...) float dispatchNormNativeF(...) double dispatchNormNativeD(...)
When the native method is a synchronized method, it further requires the monitor lock/unlock operations. For efficiency, we further define a set of separate stub functions to cover this case:
void dispatchSynchNativeV(...) int dispatchSynchNativeI(...) int64_t dispatchSynchNativeJ(...) float dispatchSynchNativeF(...) double dispatchSynchNativeD(...)
These functions perform the followings:
methodblock from %g3 to obtain the starting address of the native method.of ExecEnvmethodblock of the caller, and set to the dummy JVM frame. Again, this is required for reflection and exception handling.MonitorEnterMonitorExithandle_exception, which actually controls the transfer entirely and never returns.mb->code from the operand stack and place it appropriately into the register according to the return type.DISPATCH_NATIVE
Java bytecodes refers to classes, instance variables, and methods via symbol names. Symbols are stored in a structure called constant pool. Thus, on bytecodes execution, one must search the constant pool with the given symbols as a key, and obtain the actual address. This search is quite costly, as one must lock the constant pool region for multithreaded execution. Moreover, constant pool references occur quite frequently, and the cost of the search could dominate the overall execution time.
The JVM implementation solves this problem by modifying the bytecode on the fly. That is to say, the bytecode for constant pool access is modified to an equivalent so-called quick bytecode, which refers to the absolute address after the name has been resolved. For example, the bytecode instruction:
getfield #22 <Field Obj var>
pushes the object variable value onto the operand stack. When this instruction is first executed, the constant pool is always searched for the constant pool index #22. As a result, when we find that this variable can be accessed at a 4-byte offset from the object header, then the JVM modifies the code at runtime to the following quick version:
getfield_quick 4
then subsequently re-executes the instruction. From that point on, the quick instruction is always used, since the constant pool does not change over the execution of the program, effectively eliminating the lookup cost.
However, for native code, it is more difficult to eliminate this cost of lookup by naive application of a similar technique. A simple method would be to search all the possible symbol values in the constant pool and resolve them at once at compile time. However, constant pool resolution is not mere simple symbol resolution, but rather incurs other processing such as class loading and initialization; thus, this strategy could change the semantics of the program by changing the initialization order of classes.
The viable option is to change the native instruction code in the same manner as the interpreter. However, it is much more difficult to do for native code, which involves several native instructions per each bytecode. Since the length of the instruction sequence cannot change, this could involve insertion of several NOP instructions. Moreover, the change must be atomic, requiring some form of mutual exclusion. Moreover, the change must propagate across code caches on different processors in a multiprocessor environment.
The basic solution is as follows. For SPARCs, we place the CALL instruction to the constant pool resolution routine preceding a delay slot which contains the MOV instruction to set the index number of the symbol onto a register:call resolve mov #22,%o0
When this sequence is executed, the resolve() routine is called. There, after the constant pool is searched and the address corresponding to the index is found, the two instructions are rewritten so that now the instruction sequence places the the address (or the offset) value into a register:
sethi %hi(offset), %o0 or %o0, %lo(offset),%o0
Then, the resolve() routine returns with the resolved address in the %o0 register. The instruction immediately following the two rewritten instructions merely accesses the memory using %o0. For SPARCs, we could further optimize this as small offsets can be encoded into one instruction, and the load instruction contains a displacement field.
Although the basic idea was given, in practice for SPARCs the MOV instruction only accepts signed 13 bits as index values. In JVM, the index value can be as large as 16 bits; so, we have employed the SETHI instruction instead of the MOV instruction, allowing the usage of 22 bits. One caveat is that the encoded index value is the value obtained by left shifting the bits by 10 bits.
call resolver -> sethi %hi(offset),%o0 sethi (index<<10),%o0 -> or %o0,%lo(offset),%o0
The following functions actually searches the constant pool and resolves the offsets:
int OpenJIT_resolveField(int index)
The JVM getfield and putfield instructions are translated to call this function, which performs the followings:
CHECK_SELF_MODIFYING)methodblock of the caller. We obtain the address of the constant pool from this methodblock structure. Also, we set the address value into the JVM dummy frame in case exception happens.RESOLVE_CLASS_CONST)PATCH_SET_O0)int OpenJIT_resolveStaticField(int index)
The JVM getstatic and putstatic bytecodes are translated to call this function.
CHECK_SELF_MODIFYING)index by 10bitsmethodblock of the caller. We obtain the address of the constant pool from this methodblock structure. Also, we set the address value into the JVM dummy frame in case exception happens.RESOLVE_CLASS_CONST)PATCH_SET_O0)
u.static_address of the fieldblock.PATCH_SET_O0)int OpenJIT_resolveString(int index)
For JVM bytecodes ldc and ldc_w, if the constant pool type is CONSTANT_String, then the bytecodes are translated to call the function.
CHECK_SELF_MODIFYING)index by 10bitsmethodblock of the caller. We obtain the address of the constant pool from this methodblock structure. Also, we set the address value into the JVM dummy frame in case exception happens.RESOLVE_CLASS_CONST)PATCH_SET_O0)We modify only the CALL instruction. This is only employed when a new class is loaded on method call. This series of instructions is a combination of setting the pointer to the method block into the %g3 register, and CALLing the invoker function. Here, only the CALL instruction is modified.
sethi %hi(mb),%g3 -> sethi %hi(mb),%g3 call old_invoker -> call new_invoker or %g3,%lo(mb),%g3 -> or %g3,%lo(mb),%g3
The functions below are subject to such one-instruction modification:
int OpenJIT_invokeinterface(...)
The JVM invokeinterface bytecode is translated to call this function, which performs the followings:
CHECK_SELF_MODIFYING)methodblock of the caller.RESOLVE_CLASS_CONST)OpenJIT_invokeinterface_quickOpenJIT_invokeinterface_quick.The JVM invokeinteface_quick instruction is translated to invoke this function. Also, as indicated in Section 4.1.1, it is invoked subsequently to the invocation of OpenJIT_invokeinterface. The %g3 register must contain the predicted value of the method table. The procedure is almost same as when JVM processes the invokeinterface_quick bytecode, but differs in the following points:
We modify the instruction that sets the %g3 register, preceding the call instruction which called this function; the modification is such that the predicted value is shifted by 24 bits, and set to the upper 8-bits of the %g3 register. Since the predicated value could be old and stale, we do not lock the instruction upon modification.
void OpenJIT_invokespecial(...)
The JVM invokespecial, invokenonvirtual_quick bytecodes are translated to call this function. The function is effectively used when the method does not change for the given call site irrespective of the type of the object.
CHECK_SELF_MODIFYING)methodblock of the caller.methodblock structure, and find out whether we are invoking the methods in the ancestor classes (super).OpenJIT_invokesuper_quick, and jump to OpenJIT_invokesuper_quick().RESOLVE_NATIVE_OR_COMPILE)mb->CompiledCode.void OpenJIT_invokesuper_quick(...)
The JVM invokesuper_quick bytecode is translated to call this function. Also, it might be called after a call to the OpenJIT_invokespecial function. It performs essentially the same procedure as the JVM for this instruction.
void OpenJIT_invokestatic(...)
The JVM invokestatic instruction (in case it does require constant pool resolution) and the invokestatic_quick instruction are translated to call this function, which performs the followings, allowing direct, fast calls to static methods:
CHECK_SELF_MODIFYING)methodblock of the caller.RESOLVE_NATIVE_OR_COMPILE).mb->CompiledCodemb->CompiledCode.void OpenJIT_invokevirtual(...)
For invokevirtual bytecodes that does not require constant pool resolution, and the invokevirtual_quick instructions are translated to call this function. The procedure is similar to OpenJIT_invokestatic, but the call target of the self-modified code differs in the following way:
OpenJIT_invokestatic to make a jump to mb->CompiledCode. This allows direct jump to the target method.java.lang.Object methods:OpenJIT_invokevirtaulobject_quickOpenJIT_invokevirtual_quickSubsequently, the case analysis becomes unnecessary, speeding up the virtual call.
We modify the instruction sequence consisting of three instructions, which involves a method invocation with constant pool resolution. The general sequence is as follows:
call old_invoker -> sethi %hi(mb),%g3 sethi %g3,index<<10 -> call new_invoker illtrap -> or %g3,%lo(mb),%g3 /* delay slot */
The following functions are subject to 3 instruction modification:
void OpenJIT_invokespecial_resolve(...)
The JVM invokespecial bytecode is translated to call this function. After constant pool resolution, we perform the same process as the OpenJIT_invokespecial function.
void OpenJIT_invokestatic_resolve(...)
The JVM invokestatic bytecode is translated to call this function. After constant pool resolution, we perform the same process as the OpenJIT_invokestatic function.
void OpenJIT_invokevirtual_resolve(...)
The JVM invokevirtual bytecode is translated to call this function. After constant pool resolution, we perform the same process as the OpenJIT_invokevirtual function.
We modify the instruction sequence consisting of three instructions, which involves an access to class object with constant pool resolution. The general sequence is as follows. For this kind of sequences, for the modified target of the call instruction, the first argument of the call is the resolved address of the class object.
call old_func -> sethi %hi(mb),%o0 sethi %o0,index<<10 -> call new_func illtrap -> or %g3,%lo(mb),%o0 /* delay slot */
The following functions are subject to 3 instruction modification with %o0:
HObject *OpenJIT_new(int index)
The JVM new bytecode is translated to call this function. It performs the following steps:
CHECK_SELF_MODIFYING)index right by 10 bitsRESOLVE_CLASS_CONST)OpenJIT_new_quick (PATCH_SET_O0_and_CALL)OpenJIT_new_quick)HArrayOfObject *OpenJIT_anewarray(int index, int size)
The JVM anewarray bytecode is translated to call this function. It performs steps similar to OpenJIT_new, self-modifies the target to OpenJIT_anewarray_quick, and jumps to the OpenJIT_anewarray_quick.
HArrayOfObject *OpenJIT_multianewarray(int index, int dimensions,
stack_item *optop)
The JVM multianewarray bytecode is translated to call this function. It performs steps similar to OpenJIT_new, self-modifies the target to OpenJIT_multianewarray_quick, and jumps to the OpenJIT_multianewarray_quick.
void OpenJIT_checkcast(int index, JHandle *h)
The JVM checkcast bytecode is translated to call this function. It performs steps similar to OpenJIT_new, self-modifies the target to OpenJIT_checkcast_quick, and jumps to the OpenJIT_checkcast_quick.
bool_t OpenJIT_instanceof(int index, JHandle *h)
The JVM instanceof bytecode is translated to call this function. It performs steps similar to OpenJIT_new, self-modifies the target to OpenJIT_instanceof_quick, and jumps to the OpenJIT_instanceof_quick.
Since JVM is inherently multithreaded, caution is required for atomic updating of successive sequence of multiple instructions. If the self-modification is not atomic, other threads might try to execute the half-cooked instruction sequence, resulting in a critical error.
The current JVM supports two types of thread system. One is the green thread, and the other is the native thread. The green thread only works for uniprocessor machines, and the context switching occurs only at fixed, safe locations, and thus such problems do not occur. For native threads, however multiple threads might be executing on different processors, resulting in partially rewritten instruction sequences to be executed. Thus, it is extremely important to guarantee the atomicity of self-modification in an efficient manner. OpenJIT implements such an atomic update in the following way:
CHECK_SELF_MODIFYING()This macro checks whether the call instruction to the function which uses the macro has been modified or not. If it has been modified, then the control returns to the modified instruction of the call site, which is re-executed.
PATCH_CODE(CODE, OFFSET)This macro modifies the instruction whose offset is OFFSET from the call instruction which called the function which uses this macro. Subsequently, the instruction cache is flushed. For example, PATCH_CODE(code,4) modifies the delay slot of the call site which called the function.
Multiple instructions are modified atomically in the following way. We assume that the first instruction of the sequence of instructions to be modified is a CALL instruction, followed by NOP instructions. The function which had been called by the CALL instruction modifies the instruction sequence.
We illustrate this scheme below:
| Rewriting sequence of multiple instructions | |||||||
|---|---|---|---|---|---|---|---|
| label | Step 1 | -> | Step 2 | -> | Step 3 | -> | Step 4 |
| ... | ... | ... | ... | ||||
| Inst0 | Inst0 | Inst0 | Inst0 | ||||
| modify: | CALL A | Branch modify | Branch modify | Inst1 | |||
| NOP | NOP | Inst2 | Inst2 | ||||
| NOP | NOP | Inst3 | Inst3 | ||||
| NOP | NOP | Inst4 | Inst4 | ||||
| Inst5 | Inst5 | Inst5 | Inst5 | ||||
| ... | ... | ... | ... | ||||
One problem with this scheme is when multiple threads execute the CALL A instruction. However, since both threads will be modifying the instruction sequence (INST1 , ... , INST4) identically, this will not cause a problem (It is a little bit more subtle than this***).
For efficient execution, OpenJIT backend does not generally check for exceptions except for a few instances where explicit runtime checks are required. Instead, exceptions are checked and processed using the Unix signaling mechanism.

Figure 2 indicates how the compiled native code executes. We must check for exception occurrence when the transfer of control occurs between the compiled native code and other native code such as the JVM interpreter and runtime routines, and native methods. For example, In Figure(exception), we must check for exception for each point in the control flow marked by a star. On the other hand, for exceptions occurring with the compiled native code, we generally employ the Unix signals, and do not explicit check for exception occurrence.
By setting the Java Native Code API, the following function is called when a Unix signal occurs:
static void OpenJIT_SignalHandler(int sig, siginfo_t *info, ucontext_t *uc)
Below are the possible exceptions that might occur in runtime. Other signals are not JVM exceptions, but rather a compiler or a JVM bug.
| Signal | Purpose |
|---|---|
| SIGFPE | zero division |
| SIGSEGV | null pointer, stack overflow |
| SIGILL | array index out of bounds |
Within the OpenJIT_SignalHandler function, in order to check that the signal was indeed generated by a Java exception and not a compiler or a JVM bug, we check the instruction that caused the exception, and its operand address. For each type of exception, we perform the check in the following way, and by calling the setcontext() system call, we setup the calling frame so that the instruction causing the exception behaves as if it had called the exception generation function.
The signal SIGFPE is raised, and the exception code info->si_code is either FPE_INTOVF or FPE_INTDIV. If so, signal handler sets up the context so that it seems as if the following function had been called from the instruction that caused the exception.
void catchZeroDivide(unsigned char *pc)
The signal SIGSEGV is raised, and the exception code info->si_code is SEGV_MAPERR. In addition, the base register of the instruction that caused the exception is 0. Here is the exception generation function:
void catchNullPointer(unsigned char *pc)
The signal SIGSEGV is raised, and the exception code info->si_code is SEGV_MAPERR. In addition, the instruction that caused the exception is ld [%sp + constant]. Here is the exception generation function:
void catchStackOverflow(unsigned char *pc)
The signal SIGILL is raised, and the instruction that caused the exception is a trap instruction, and the trap code is ST_RANGE_CHECK. Here is the exception generation function:
void catchArrayIndexOutOfBounds(unsigned char *pc, int index)
This function is slightly different, in that the index of the array must be given as the second parameter. For this reason, before we perform setcontext, we must check the value of the register which was used as a operand to calculate the out-of-bounds condition.
FIND_EXCEPTION_FRAME(pc, ee)This is a macro used by the exception generation functions described above in order to identify the method that caused the exception. It performs the following steps:
methodblock structure into the dummy JVM frame. fillInStackTrace (described later in Section 5.4.).handle_exception)bool_t handle_exception (ExecEnv *execEnv)
This function traces the compiled native code stack, and finds the corresponding exception handler, and jumps to the handler. As is with C longjump(), it makes a jump leapfrogging the nested function calls. Because SPARC has register windows, they must be restored appropriately during leapfrogging. Here are the steps:
while(1) {
/* delete the stackframe of the runtime routine */
while(%i7(address of the caller) is within the runtime routine) {
restore /* recover the register window */
}
if (%i7(return address) is not a compiled native code) {
/* Return to the JVM interpreter loop */
return FALSE;
}
/* Set the lastpc. Needed when returning to the interpreter loop? */
ee->current_frame->lastpc = %i7
Extract the pointer to the methodblock structure from %fp, and
set it to the variable mb
/* Find the exception handler for the caught exception within mb */
new_pc = JITProcedureFindThrowTag(ee, mb, ee->exception.exc, %i7)
if (new_pc != 0) {
/* An exception handler is found! */
exceptionClear(ee) /* Clear the exception flag */
/*
* The exception handler for the compiled native code assume
* that the pointer to the object that caused the exception
* is in %i7
*/
%i7 = ee->excetion.exc
restore /* restore the register window */
jump new_pc /* Jump to the exception handler */
}
/* Exception handler is not found */
if (mb is a synchronized method) {
/* unlock the monitor lock */
/* The monitor object is stored in %fp[-1] */
monitorExit(%fp[-1])
}
restore
}
fillInStackTraceJDK calls the SignalError function when an exception occurs. This function in turns calls fillInStackTrace(),. Also, java.lang.Throwable class has a method fillInStackTrace, allowing the user program to obtain the status of the current Java method, and the trace of the stackframe.
The code generated by the OpenJIT compiler does not generate a Java frame when compiled native code is called from another native code. As a result, JVM cannot trace the stackframe. To solve this problem, the JDK prepares the following API:
JavaFrame *JITCompiledFramePrev(JavaFrame *frame, JavaFrame *buf)
Other than fillInStackTrace, this function is used to obtain the trace of the stackframe. JVM basically uses the following algorithm to walk the stack to obtain the trace:
{
JavaFrame *frame, buf;
frame = ExecEnv->current_frame;
while(frame) {
if (frame->current_method->fb.access & ACC_MACHINE_COMPILED) {
frame = CompiledFramePrev(frame, &buf);
} else {
frame = frame->prev;
}
}
}
Thus, before JITCompiledFramePrev is called, ExecEnv (the execution environment structure) current_frame must have the Java fame of the compiled native code. For this purpose, when there is a possibility that an exception may occur upon calling a JVM function from the OpenJIT runtime routine, we must also set the JVM frame in the ExecEnv->current_frame.
For OpenJIT, we judged that it is too expensive to generate a JVM frame each time this happens. Instead, we generate a dummy JVM frame only when the control flow transfers from the compiled native code into the internals of the JVM, and set it to ExecEnv->current_frame. When the OpenJIT runtime routine calls a JVM function, we merely set the current_method of the dummy frame.
We show the other OpenJIT runtime functions that are called from the compiled native code that the OpenJIT compiler generates. The compiled native code may also call a C library function or a JVM function. The table below indicates where the called functions are being defined.
| JVM Instruction | Runtime Function | Library |
|---|---|---|
| anewarray_quick | HArrayOfObject *OpenJIT_anewarray_quick(ClassClass *array_cb, int size) | OpenJIT |
| athrow | void OpenJIT_athrow(HJava_lang_Object *obj) | OpenJIT |
| checkcast_quick | void OpenJIT_checkcast_quick(ClassClass *cb, JHandle *h) | OpenJIT |
| d2l | int64_t __dtoll(double d) | C |
| dcmpg | int OpenJIT_dcmpg(stack_item *p) | OpenJIT |
| dcmpl | int OpenJIT_dcmpl(stack_item *p) | OpenJIT |
| drem | double OpenJIT_drem(stack_item *p) | OpenJIT |
| f2l | int64_t __ftoll(float f) | C |
| fcmpg | bool_t OpenJIT_fcmpg(float *p) | OpenJIT |
| fcmpl | bool_t OpenJIT_fcmpl(float *p) | OpenJIT |
| frem | float OpenJIT_frem(float *args) | OpenJIT |
| instanceof | bool_t OpenJIT_instanceof(int index, JHandle *h) | OpenJIT |
| l2d | double OpenJIT_l2d(signed hi, unsigned lo) | OpenJIT |
| l2f | float OpenJIT_l2f(signed hi, unsigned lo) | OpenJIT |
| lcmp | bool_t OpenJIT_lcmp(long long x, long long y) | OpenJIT |
| ldiv | int64_t __div64(int64_t x, int64_t y) | C |
| lmul | int64_t __mul64(int64_t x, int64_t y) | C |
| lrem | int64_t __rem64(int64_t x, int64_t y) | C |
| lshl | uint64_t longOpenJIT_lshl(signed hi, unsigned lo, unsigned b) | OpenJIT |
| lshr | uint64_t longOpenJIT_lshr(signed hi, unsigned lo, unsigned b) | OpenJIT |
| lushr | uint64_t longOpenJIT_lushr(signed hi, unsigned lo, unsigned b) | OpenJIT |
| monitorEnter | void monitorEnter(unsigned int key) | JDK |
| monitorExit | void monitorExit(unsigned int key) | JDK |
| multianewarray_quick | HObject *OpenJIT_multianewarray_quick(ClassClass *array_cb, int dimensions, stack_item *optop) | OpenJIT |
| new_quick | HObject *OpenJIT_new_quick(ClassClass *cb) | OpenJIT |
| newarray | JHandle *OpenJIT_newarray(int type, int size) | OpenJIT |
We covered the runtime structure of the OpenJIT backend system. For the details of how the JVM instructions are translated, and runtime functions are called, the readers are referred to the files in org/OpenJIT/Sparc.java. The layout of the stackframe of the compiled native code is described in a companion document OpenJIT Backend Compiler Internal Specification.