The Java Virtual Machine Specification does not define how the virtual machine interacts with events that arrive from the host operating system or from the target device. The KVM implementation, however, provides a variety of mechanisms that were designed to facilitate the integration of the KVM with the event system mechanisms of the host operating system or device.
There are four ways in which notification and handling of events can be accomplished in KVM:
Different solutions may be appropriate for different ports of the KVM, depending on which user interface libraries are supported, what kinds of networking libraries are used, and so forth.
By synchronous notification we refer to a situation in which the KVM performs event handling by calling a native I/O or event system function directly from the virtual machine. Since the KVM has only one physical thread of control inside the virtual machine, no other Java threads can be processed while the native function is being executed, and no VM system functions such as garbage collection can occur either. This is the simplest form of event notification, but there are many situations in which this solution is quite acceptable, provided that the person designing the native functions is careful enough to keep the native functions as short and efficient as possible.
For instance, writing a datagram into the network can typically be performed efficiently using this approach, since typically the datagram is sent to a network stack that contains a buffer and the time spent waiting for the event to complete is very small. In contrast, reading a datagram is often a very different story, and is often handled better using the other solutions described below. Using a native function to wait until a whole datagram is received would block the whole KVM while the read operation is in progress.
Often event handling can be implemented efficiently using a combination of native and Java code. This is a simple way to allow other Java threads to execute while waiting for an event to complete. When using this approach, a polling Java loop is normally put somewhere in the Java runtime libraries so that the loop is hidden from applications. The normal procedure is for the runtime library to initiate a short native I/O operation and then repeatedly query the status of the I/O operation until it is finished. The polling Java code loop should always contain a call to Thread.yield
so that other Java threads can be allowed to run efficiently.
This method of waiting for event notification is very easy to implement and is free of any complexities typically associated with genuinely asynchronous threads (such as requiring critical sections, semaphores or monitors.) There are two disadvantages with this design. First, CPU cycles are needed to perform the Java-level polling that could otherwise be used to run application code (although the overhead is usually very small.) Second, due to the interpretation overhead, there may be some extra latency associated with event notification (especially if you forget to call Thread.yield
in the polling Java code loop.) Again, this overhead is usually negligible in all but most time-critical applications.
The third approach to implement event handling is to use the bytecode interpreter periodically make calls to the native event handling operations. This approach is a variation of the synchronous notification approach described above. This approach was originally used extensively in the KVM, for example, to implement GUI event handling for the Palm platform.
In this approach, a native event handling function is called periodically from the interpreter loop. For performance reasons this is not normally done before every bytecode, but every few hundred bytecodes or so. This way the cost of performing event handling is well amortized. By changing the number of bytecodes executed before calling the event handling code, the virtual machine designer can control the latency of event delivery versus the CPU time spent looking for a new event. The smaller the number, the smaller latency and the larger CPU overhead. A large number reduces CPU overhead but increases the latency in event handling.
The advantage of this approach is that the cost in performance is less than polling in Java, and the event notification latency is more predictable and controllable. The way this approach works is closely related to asynchronous notification described in the next subsection.
The original KVM implementation supported only the three event handling implementations discussed above. However, in order to support truly asynchronous event handling, some new mechanisms have been introduced.
By asynchronous notification we refer to a situation in which event handling can occur in parallel while the virtual machine continues its execution. This is generally the most efficient event handling approach and will typically result in a very low notification latency. However, this approach generally requires that the underlying operating system provides the appropriate facilities for implementing asynchronous event handling. Such facilities may not be available in all operating systems. Also, this approach is quite a bit more complex to implement, as the virtual machine designer must be aware of possible locking and mutual exclusion issues. The reference implementation provides some examples that can be used as a starting point when implementing more device-specific event handling operations.
The general procedure in asynchronous notification is as follows. A thread calls a native function to start an I/O operation. The native code then suspends the thread's execution and immediately exits back to the interpreter loop, letting other threads continue execution. The interpreter then selects a new thread to run. Some time later an asynchronous event occurs and as a result some native code is executed which resumes the suspended thread. The interpreter then restarts the execution of the thread that had been waiting for an event to occur.
At the implementation level, there are two ways to implement such asynchronous notification. One is to use native (operating system) threads, and the other is to use some kind of software interrupt, callback routine or a polling routine.
In the first case, before the native function is called and the Java thread is suspended, a new operating system thread is created (or reawakened) and it is this thread which enters the native function. There is now an additional native thread of control running inside the virtual machine. After the native I/O thread is started, the order of execution inside the virtual machine is no longer fully deterministic, but depends on the occurrence of external events. Typically, the original thread starts executing another Java thread in the interpreter loop, and the new thread starts the I/O operation with what is almost always a blocking I/O operation to the operating system.
It is important to note that the native I/O function will execute out of context meaning that the context of the virtual machine will be a different thread. A special set of C macros were written that will hide this fact for the most part, but special care should be taken to be sure that no contextual pointers are used in this routine. When the blocking call is finished the native I/O thread resumes execution and unblocks the Java thread it was representing. The Java thread is then rescheduled, and the native I/O thread is either destroyed, or placed in a dormant state until it needs to be used again. The Win32 port of the KVM reference implementation does this by creating a pool of I/O threads that are reused when
I/O is to be performed.
The second implementation of asynchronous event handling can be done by utilizing callback functions associated with I/O requests. Here the native code is entered using the normal interpreter thread, I/O is started and then when the I/O operation is completed a callback routine is called by the operating system and the Java thread is unsuspended. In this scenario the native code is split into two routines, the first being a routine that starts the I/O operation and the second invoked when I/O is completed. In this case the first routine runs in the context of the calling Java thread, and the second one does not.
The final, less efficient variation of asynchronous event handling is where the I/O routine is polled for completion by the interpreter loop. This is very similar to the callback approach except that the second routine is called repeatedly by the interpreter to check if the I/O has finished. Eventually when the I/O operation has completed the routine unblocks the waiting Java thread. This calling of the native code by the interpreter is always done even when there are no pending events, and the native code must determine what Java threads should be restarted.
Synchronization issues. It is very important to remember that in the cases where a separate native event handling thread or callback routine is used, the code for event handling may interrupt the virtual machine at any point. Therefore, the person porting the virtual machine must remember to add critical sections, monitors or semaphores to all locations where the program may be manipulating common data structures and a possible mutual exclusion problem might occur. The most obvious shared data structures are the queues of suspended and active Java threads. These are always manipulated using special routine in the virtual machine that is already properly synchronized. If there are any other shared data structures they must be synchronized in the native code. Failure to do this correctly will produce spurious bugs that are very hard to debug.
When native event handling code is called, its parameters will be on the stack for the calling Java thread. These are popped off the stack by the native code, and the if there is a result value to be returned this is pushed onto the Java stack just prior to resuming the execution of the thread. Native parameter passing issues are discussed in Chapter 11.
Because native event handling code can access object memory, there are possible garbage collection issues especially when running long, asynchronous I/O operations. In general, the garbage collector is prevented from running when there is any native code is running. This is a problem when certain long I/O operations are performed. The most obvious case is waiting for a incoming network request. To solve this problem two functions called decrementAsyncCount
and incrementAsyncCount
are provided. The first allows the garbage collector to start, and the second prevents the collector from starting, and waits for it to stop if it was running.
It should be noted that if an object reference is passed to a native method, but no other reference to it exists in Java code after the call to incrementAsyncCount
, the object could be reclaimed accidentally by the garbage collector. It is hard to think of a realistic scenario where this could occur, but the possibility should be kept in mind. A possible example of such code is the following:
Here the only reference to the byte array object exists on the parameter stack to the native function. If the native code calls incrementAsyncCount
after popping the parameter from the stack the array could be garbage collected.
The event handling implementation in KVM is composed of two main layers that both need to be taken into account when porting the KVM onto new hardware platforms.
At the top of the interpreter loop is the following code (starting from KVM 1.0.2, this code is actually located in macros):
The standard rescheduling code performs the following operations.
For performance reasons, the operations above are implemented as macros that are, by default, defined in VmCommon/h/events.h
. It is here that device-specific event handling code can be placed. By default, the isTimeToReschedule
macro decrements a global counter and tests for it being zero. When it is zero the second macro is executed. The idea here is for the reschedule to be executed only once for a fairly large number of bytecode executions. As the name implies, reschedule is where the thread context switching is done, if necessary.
The second layer in event handling implementation is the function
If a new event is available from the host operating system, this function must call a special function called StoreKVMEvent
to make the details of the event available to the KVM. If no new events are available from the host operating system, then the function can simply return.
The arguments to the GetAndStoreNextKVMEvent
function are as follows:
TRUE
, this function should wait for as long as necessary for an event to occur (used for battery conservation as described below.)FALSE
, this function should wait until at most waitUntil
for an event to occur.
Some battery conservation features were included in the reference implementation of these functions. This is to pass to the event checking function the “forever” flag or the maximum wait time. If there are no pending events, the native implementation of the GetAndStoreNextKVMEvent
function can then put the device “to sleep” until the next event occurs. Battery conservation issues are discussed in more detail in the next subsection.
Most KVM target devices are battery-operated, and the manufacturers of these devices are typically extremely concerned of excessive battery power consumption. To minimize battery usage, KVM is designed to stop the KVM interpreter loop from running whenever there are no active Java threads in the virtual machine and when the virtual machine is waiting for external events to occur. This requires support from the underlying operating system, however.
In order to take advantage of the power conservation features, you must port the following low-level event reading function
so that it calls the host system specific sleep/hibernation features when the virtual machine calls this function with the forever
argument set TRUE
. The KVM has been designed to automatically call this function with the forever
argument set TRUE
if the virtual machine has nothing else to do at the time.
This allows the native implementation of the event reading function to call the appropriate device-specific sleep/hibernation features until the next native event occurs.
Additionally, the macro SLEEP_UNTIL(wakeupTime)
should be defined in such a fashion that the target device goes to sleep until wakeupTime
milliseconds has passed.
KVM Porting Guide , CLDC 1.1 |
Copyright © 2003 Sun Microsystems, Inc. All rights reserved.