2. Issues with embedded Java
Sun's motto for Java is "Write once, run anywhere". As
part of that, Sun has been pushing Java as also suitable for embedded
systems, announcing specifications for "Embedded Java" and
"Personal Java" specifications. The latter (just published
at the time of writing) is primarily a restricted library that a Java
application can expect.
"Embedded systems" covers a large spectrum from 4-bit
chips with tens of bytes of RAM, to 32- and 64-bit systems with
megabytes of RAM. I believe it will be very difficult to squeeze a
reasonably complete implementation of Java into less than one
MB. (However, people have managed to squeeze a much reduced Java into
credit-card-sized "smart cards" with about 100kB.) In
general, there is less memory available than in the typical desktop
environment where Java usually runs. Those designers that have been
leery of C++ because of performance concerns (perceived or real) will
not embrace Java. On the other hand, those that have been leery of C++
because of its complexity will find Java easier to master.
2.1 Advantages of Java
Java has a number of advantages for embedded systems. Using classes
to organize the code enforces modularity, and encourages data
abstraction. Java has many useful and standardized classes (for
graphics, networking, simple math and containers,
internationalization, files, and much more). This means a designer can
count on having these libraries on any (full) implementation of
Java.
Java programs are generally more portable than C or C++ programs:
- The size of integer, floats, and character is defined by the language.
- The order and precision of expression evaluation is defined.
- Initial values of fields are defined, and the languages require that local variables be set before use in a way that the compiler can check.
In fact, the only major non-determinacy in Java is due to
time-dependencies between interacting threads. Safety-critical
applications will find the following features very useful:
- More disciplined use of pointers, called "references" instead, provides "pointer safety" (no dangling references; all references are either null or point to an actual object; de-referencing a null pointer raises an exception).
- Array indexing is checked, and an index out of bounds raises an exception.
- Using exceptions makes it easier to separate out the normal case from error cases, and to handle the error in a disciplined manner.
The portability of Java (including a portable binary format) means
that an application can run on many hardware platforms, with no
porting effort (at least that's the theory).
2.2 Code compactness
It has been argued that even for ROM-based embedded systems (where
portability is not an issue), it still makes sense to use a
bytecode-based implementation of Java, since the bytecodes are
supposedly more compact than native code. However, it is not at all
clear if that is really the case. The actual bytecode instructions of
the Fib class (a program to calculate Fibonacci numbers, which is
discussed in the section, 5. Status) only
take 134 bytes, while the compiled instructions for i86 and Sparc take
373 and 484 bytes respectively. (This is if we assume external classes
are also pre-compiled; otherwise, 417 and 540 bytes are needed,
respectively.) However, there is quite a bit of symbol table
information necessary, bringing the actual size of the
Fib.class file up to 1227 bytes. How much space will
actually be used at run-time depends on how the symbolic (reflective)
information is represented - but it does take a fair bit of
space. (Pre-compiled code also needs space for the reflective data
structure about 520 bytes for this example.) Out tentative
conclusion is that the space advantage of bytecodes is minor at best,
whereas the speed penalty is major.
2.3 Space for standard run-time
In addition to the space needed for the user code, there is also a
large chunk of fixed code for the run-time environment. This includes
code for the standard libraries (such as java.lang), code
for loading new classes, the garbage collector, and an interpreter or
just-in-time compiler.
In a memory-tight environment, it is desirable to be able to leave
out some of this support code. For example, if there is no need for
loading new classes at run-time, we can leave out the code for reading
class files, and interpreting (or JIT-compiling) bytecodes. (If you
have a dynamic loader, you could still down-load new classes, if you
compile them off-line.) Similarly, some embedded applications might
not need support for Java's Abstract Windowing Toolkit, or
networking, or decryption, while another might need some or all of
these.
Depending on a conventional (static) linker to select only the code
that is actually needed does not work, since a Java class can be
referenced using a run-time String expression passed to the
Class.forName method. If that feature is not used, then a
static linker can be used.
The Java run-time needs to have access to the name and type of
every field and method of every class that is loaded. This is not
needed for normal operation (especially not when using precompiled
methods); however, a program can examine this information using the
java.lang.reflect package. Furthermore,
the Java Native
Interface (JNI, a standard ABI for interfacing between C/C++ and Java)
works by looking up fields and methods by name at run-time. Using the
JNI thus requires extra run-time memory for field and method
information. Since the JNI is also quite slow, an embedded application
may prefer to use a lower-level (but less portable) Native
Interface.
Because applications and resources vary so widely, it is important
to have a Java VM/run-time than can be easily configured. Some
applications may need access to dynamically loaded classes, the Java
Native Interface, reflection, and a large class library. Other
applications may need none of these, and cannot afford the space
requirements of those features. Different clients may also want
different algorithms for garbage collection or different thread
implementations. This implies the need for a configuration utility, so
one can select the features one wants in a VM, much like one might
need to configure kernel options before building an operating
system.
2.4 Garbage collection
Programmers used to traditional malloc/free-style heap management
tend to be skeptical about the efficiency of garbage collection. It is
true that garbage collection usually takes a significant toll on
execution time, and can lead to large unpredictable pauses. However,
it is important to remember that is also an issue for manual heap
allocations using malloc and free. There are many very poorly written
malloc/free implementations in common use,
just as there are inefficient implementations of garbage collection.
There are a number of incremental, parallel, or generational
garbage collection algorithms that provide performance as good or
better than malloc/free. What is
difficult, however, is ensuring that pause times are
boundedi.e., a small limit on
the amount of time a new can take, even if garbage collection is
triggered. The solution is to make sure to do a little piece of
garbage collection on each allocation. Unfortunately, the only known
algorithms that can handle hard-real time either require hardware
assistance or are very inefficient. However, "soft"
real-time can be implemented at reasonable cost.
|