When referring to the VM, the referral is always to
the headless language and core classes engine only (i.e., no UI). The majority of shipped devices include a
port of Sun's CLDC-HI HotSpot VM 1.1 implementation (which implements
v1.1 of the CLDC specification). However, the VM is a replaceable module
in the Java ME subsystem; for example, in S60 3rd Edition FP2, the
included VM is J9 from IBM.
VM technology contains a wealth of theory and
practical tricks that make the JVM one of the most optimized pieces of
code written for mobile phones. To peek into some of those goodies, we
use the open-source CLDC-HI HotSpot VM as a reference (all sources are
available in the PhoneME project).
Just one note before we continue. Please do not use
the term 'KVM' ... because there is no KVM any more! The KVM was a major
milestone in Java ME evolution but it was put behind a glass box in the
Java museum a long time ago. Symbian OS v7.0s devices (e.g., the
legendary Nokia 6600) were shipped with Monty, which was an early
version of CLDC-HI HotSpot. So the term 'KVM' is an ancient word which
belongs to the Java hall of fame, but does not exist on today's devices.
These come with VMs that are much more advanced than the KVM, which was
slow (it was an interpreter only, no JIT) and had high memory usage.
We previously said that one of the few areas where
the Java ME subsystem can reuse existing solutions is VM technology. So
the VM needs to be ported to Symbian OS – which does not mean that the
whole VM needs to be written from scratch! The VM needs to be written in
a standard dialect of C or C++ that is compatible with the Symbian OS
toolchain. Additionally, the VM must define and use some porting layer
through which it receives basic services from the underlying platform.
Our reference VM, the CLDC-HI HotSpot, is written in standard C++ and
has a minimal porting layer. Interested readers can examine the
implementation in the phoneME project repository.
(Just to remind you, we are currently talking about the headless VM,
not the full-blown Java ME run-time environment which includes MIDP.)
Below you can find a few examples of some of the
CLDC-HI HotSpot porting layer interfaces that need to be implemented
using the native operating-system APIs:
// All VM output goes through this method
void JVMSPI_PrintRaw(const char* s);
// Returns the time as System.currentTimeMillis()
static jlong java_time_millis();
// Allocate a memory chunk for the Object heap that can expand or shrink
address OsMemory_allocate_chunk(size_t initial_size, size_t max_size,
size_t alignment);
// Flush the CPU instruction cache to ensure that the code generated by
// JIT is loaded from main memory when it is executed.
void OsMisc_flush_icache(address start, int size);
The first thing that is done when porting a VM is to
build it on the platform and run a headless application – a single class
with a single static main function that does not do much more than
print to the console. After successfully implementing the porting layer
and building the VM, we can run our first headless application. Cool.
Now what's next? We need to add the Profile on top of the VM. From here
things accelerate and it becomes more and more interesting.
As we said, the Java ME subsystem includes a Profile
(e.g., MIDP 2.0) implemented by Symbian which is VM-agnostic so that
licensees can use VMs from different VM vendors. We also mentioned an
insulation layer between the Profile and VM proprietary interfaces.
Let's take a look at this insulation layer, in relation to the CLDC-HI
HotSpot VM: CLDC-HI defines the KNI interface which is designed to be a
logical subset of JNI and provides functions for instance field access,
to objects, strings, arrays, classes, interface operations, and so on.
One of the key goals of KNI is to isolate the external code (e.g.,
Profile implementation code) from the internal implementation details of
the CLDC-HI VM. However, while succeeding in doing that, the KNI is
still VM-specific and is not portable across VMs. For example, KNI uses a
register-based approach for retrieving method parameters from the stack
frame. Use of the KNI interfaces by the MIDP layer would create a
direct dependency on CLDC-HI HotSpot.
To allow replacement of the VM, Symbian OS defines
the KJNI which is a subset of the Java SE JNI. All native code that is
required to perform such operations uses only the KJNI. Porting a new VM
to Symbian OS requires implementing the KJNI in terms of the
VM-specific interface. For example, the implementation of the KJNI for
CLDC-HI uses the KNI. As a result, switching between VMs can be done
almost seamlessly, from the Profile perspective.
Figure 1 depicts both the porting layer and the KJNI layer in the context of Symbian OS and the MIDP layer.
VM technology requires quite a few tricks to create a
hosted run-time environment on top of a native platform. There are VMs for other languages that do
similar operations, such as interpreting instructions or managing
memory, but Java technology took the theory and optimized it to the
maximum. There is an order of a few magnitudes between the complexity of
the Java VM and the complexity of VMs for languages such as Python or
Ruby. So we now give a few examples of mechanisms in the VM.
1. VM Threading Model
The Java threading model can vary between different
VM implementations. One model maps Java threads to native OS threads;
another, the lightweight threading (LWT) model, implements threads
entirely inside the VM. Each model has its own pros and cons. A native
threading model means that every Java thread is scheduled by the
operating system kernel with every running thread in the operating
system. An LWT model implies much easier synchronization and is easily
portable to many platforms. J9 uses a native threading model and CLDC-HI
HotSpot uses an LWT model. As an example, we take a quick look into the
LWT model in CLDC-HI.
At VM startup, the system classes are bootstrapped and a main Java thread object is allocated (see VM::start() and Universe:: bootstrap()).
After the main Java thread is initialized, its execution stack is set
up with a first stack frame corresponding to the main method (see Thread::initialize_main(), Thread::setup_lightweight_stack() and JVM::load_main_class()). At that point, the VM is ready to start executing Java bytecode by calling JVM::run(). This is how CLDC-HI is initialized.
Let's take a short look at how the LWT emulates multiple Java threads on top of a single native OS thread (see Figure 2).
The VM runs on a single native OS thread, named the primordial thread.
During the execution of the primordial thread, either bytecodes in the
context of a Java thread can be executed or no Java bytecodes are
executed and the VM runs other internal required functionality.
The scheduling of Java threads is managed by the LWT Scheduler (see src/vm/share/runtime/Scheduler.cpp). For example, switching between Java threads is performed in Scheduler::switch_thread().
The switching between execution of the current Java thread and the
primordial thread is achieved by two assembler code fragments in the
interpreter: current_thread_to_primordial and primordial_to_current_thread. The code for those two assembler fragments is generated in src/vm/cpu/arm/InterpreterStubs_arm.cpp.
2. Linkage of Native Operations
Although there is no JNI in CLDC, every Profile and
its libraries still need to invoke native operations. Java SE (and CDC)
has a JNI mechanism in which Java code can instruct the VM to load a
native library and define Java methods that are implemented in C. In
Java SE, this mechanism is dynamic – the loading of the library and
lookup of the corresponding C function is done at run time.
CLDC-HI HotSpot takes a static approach more suitable
to constrained mobile environments. The native invocation mechanism is
available only to system classes and not to applications. System classes
define the methods that are implemented in native code in a similar way
but are not required to instruct loading of libraries. At build time,
tables that map Java native methods to their C counterparts are built
and all the linkage is done statically. No dynamic lookup is performed
at run time and only pre-built native functions can be invoked by the
VM.
3. Garbage Collection and Internal Finalization
There are various relatively small mechanisms that
are built as extensions to the supplied Configuration. Although small in
size and complexity, they are nevertheless quite important for internal
implementation or proper execution of the run-time process. For
example, the Configuration should be extended with a mechanism that
ensures that all native resources are freed before the owning Java
object is reclaimed by the garbage collector (GC). An internal mechanism
of Object, finalization, was introduced which ensures that
native resources are still released even when application code has not
properly released them. This is a simple mechanism in which instances of
internal system classes register themselves for internal object
finalization. When memory is reclaimed by the GC, every object that is
not referenced any more and was registered for finalization can release
native resources by executing an internal finalization method. This is a
simple mechanism indeed, but highly important to free up acquired
resources to allow other multitasking applications or system processes
to acquire these resources.