Article
Java Café: Java and Linux team up for embedded systems Part II
December 4, 2000

In the last installment on using Java and Linux in embedded systems designs, I talked about the advantages Java and Linux bring to the table and how a combined Java/Linux platform gives you a head start in building Internet or network-aware embedded products and getting them to market quickly.
In this article and the next, I focus on some of the key tradeoffs to consider when you're developing for a Java/Linux platform. This week I evaluate the footprint, performance, and tools issues, and in the next column, I'll give you some guidelines about memory management and determinism to help you choose a JVM suitable for your application.
Contents
Tradeoffs
Footprint
Performance
Tools
Tradeoffs
Despite its advantages (e.g., hardware abstraction, reduced footprint, tight integration), a Java/Linux platform presents a number of issues and tradeoffs that a successful developer will have to navigate through. Three important issues for you to consider are memory footprint, performance, and tools.
Return to Table of Contents
Footprint
Although the Linux kernel is scalable, there are limits to footprint reduction. Using a standard Red Hat 6.2 configuration utility, a basic kernel with networking support, including drivers, a TCP/IP stack, and an NFS client, can require up to 512 KB compressed on a floppy diskette or in ROM. The kernel decompresses into RAM at boot time.
Additional Linux executables, the Java platform itself, and any needed application classes further increase ROM requirements. An embedded JVM will take around 128 KB, whereas standard class libraries often require another 1 MB or more of additional ROM. For instance, if the java.awt graphics package is going to be used, you should expect to add another 1–2 MB of Java classes and support routines.
A common way to reduce nonvolatile storage needs in Linux is to use a compressed initial RAM disk image that automatically expands into RAM at boot time. For example, a 1.44-MB Linux boot floppy with a 512-KB compressed, network-capable kernel and a 512-KB compressed RAM disk image containing a JVM and the lang, io, net, and util Java libraries still leaves about 400 KB for application classes. A simple "hello world" class takes less than 500 bytes, so you can do quite a bit with 400 KB.
RAM requirements depend on the application, of course, but an off-the-shelf Linux distribution won't run very well in less than 8 MB. Embedded Linux vendors are working to reduce both ROM and RAM requirements. Java RAM requirements largely depend on your performance choices, but if you use static linking tools to make Java byte codes executable from ROM, you can reduce RAM usage. Without static linking, classes must be linked and loaded into RAM at execution time.
Return to Table of Contents
Performance
Before I talk about Java performance, it is important to understand how a Java program executes on an embedded device. As an example, let's see how this is done with NewMonics PERC. Diagram 1 shows the Java execution model.

First, Java source code files are compiled into class files using a standard Java-to-byte-code compiler. Byte codes are the instruction format of the Java Virtual Machine specification and can be executed by any compatible JVM on any processor and operating system, without modification.
Class files are loaded directly into a Java Virtual Machine, like PERC, and executed by the interpreter in that JVM. The classes loaded in this way are stored in RAM. You can also load class files into RAM through a Just-in-Time (JIT) compiler. The JIT compiler translates byte codes into sequences of native instructions for the processor on which the JVM is running. The compiled classes can be then be executed directly without the interpreter.
A third way to execute a Java program is to pass the class files through a static linker like the PERC ROMizer. This tool performs a number of functions. First, it pre-links classes into a form that can be executed directly from within a target executable image, even if that image is stored in ROM. Second, it can optionally pre-compile byte codes into native instructions for the target processor via Ahead-of-Time (AOT) compilation. With this method, AOT compiled classes are also executed directly from ROM, and the ROMizer generates an object file with statically linked and AOT-compiled classes, which is then linked with a run-time library containing the rest of the Java Virtual Machine to create a target executable image.
Notably, a run-time library is needed if the Java code is AOT compiled into native instructions since the run-time library contains more than just the interpreter. It also provides memory management, thread management, and any native-code support for the standard Java libraries.
Early Sun Microsystems releases of the Java platform had a byte-code interpreter that was slow compared to C or C++. Despite significant speed improvements, a Java interpreter is still 10-20 times slower than C or C++.
As mentioned earlier, some vendors have developed JIT compilers (e.g., NewMonics' QuickPERC JIT) that compile Java byte codes into native code for the target processor on the fly as they are loaded into memory. JIT compilation boosts performance, but the compilation time may not be worth it, especially if the code doesn't contain loops or only runs once.
An adaptive JIT compiler, such as the one offered by Insignia Solutions, monitors execution and only compiles code that runs frequently. A "hotspot" JIT compiler such as the one found in Sun Microsystems' JDK 1.3 recompiles the most frequently executed code with higher levels of optimization. RAM footprint is higher with JIT compilation than with interpreted code because the native instructions are placed in RAM and are less compact than Java byte codes (native code can be four to eight times larger than byte codes).
NewMonics' QuickPERC AOT and HP's TurboChai offer Ahead-of-Time (AOT) compilation. AOT compilers are similar to traditional C or C++ compilers. They generate native code that is statically linked with the Java runtime environment prior to being programmed into ROM or flash memory. This method avoids run-time compilation at the expense of increased ROM footprint.
Table 1 summarizes the relative speed, ROM footprint, and RAM footprint tradeoffs of the various performance features available from embedded Java vendors.

By moving from interpreted to compiled code, JIT and AOT compilers improve performance by as much as ten or more times. Adaptive JIT is faster than conventional JIT because it doesn't compile all classes, whereas AOT provides the highest speed by avoiding run-time compilation entirely and employing more robust optimization techniques.
Assuming that the JVM and Java classes are stored in ROM, the JIT and adaptive JIT compiler options increase ROM footprint by the size of the JIT compiler itself, with the adaptive JIT being more complex and therefore a bit larger. An AOT compiler uses the most ROM to store the compiled native code.
If you use a static linking tool like the NewMonics ROMizer to make byte codes executable from ROM, the RAM footprint requirement for interpreted code is relatively low. Conventional JIT compilers use the most RAM because all Java classes are compiled into RAM. Adaptive JIT compilers, on the other hand, use less RAM by only compiling frequently used byte codes. AOT-compiled code uses the same amount of RAM as interpreted byte codes executing from ROM.
Note that the footprint tradeoffs shown in Table 1 are for code, not data. Memory used for data by a given application is the same whether it is interpreted, JIT compiled, or AOT compiled. Also keep in mind that these are not all-or-nothing choices. A good JVM vendor lets you decide which parts of your application to AOT compile for speed and which to run interpreted or JIT compiled for lower footprint.
Return to Table of Contents
Tools
You need some basic tools to develop Java/Linux-based embedded systems. The familiar GNU gcc compiler toolchain is used to build the Linux kernel and utilities for many different target processors. Graphical IDE front ends to gcc and the gdb debugger are also commercially available from companies like Red Hat/Cygnus and Motorola/Metrowerks, or you can use the gcc tools to cross-compile for Linux targets from non-Linux hosts.
On the Java tools side, a Java IDE, which includes a Java-to-byte-code compiler and a Java source-code debugger that allows remote debugging, is essential. Commercial Java IDEs are available from many sources, but it's important to avoid tools that only debug your code on the host machine or only with a specific JVM. A JVM and debugger supporting the Java Debug Wire Protocol (JDWP) standard provide remote debugging over a network or serial connection to your target system.
The JVM vendor should provide tools that link and package the JVM and classes for storage and execution from ROM or flash memory. These tools statically link the packaged Java classes so their byte codes are executable from ROM. Alternatively, if a filesystem is available, some or all of the classes can be stored separately, usually in a ZIP or JAR archive format file.
Many embedded Java vendors provide tools to reduce footprint requirements by eliminating unused methods from classes. These "tree shakers" scan code-execution paths and "shake out" unused fields and methods. However, they prevent subsequent dynamic loading of new application classes that depend on classes or methods previously eliminated. If you want to use a tree shaker, look for one that lets you specify the core classes that should be left intact.
Lastly, embedded Java vendors may supply an AOT compiler for generating native code from Java source or class files to improve performance. The footprint tradeoffs are important to consider, so make sure you have the option of AOT-compiling selected classes for optimized performance and allowing other classes to be interpreted or JIT compiled.
Embedded systems developers in markets that demand high reliability must follow a "ship what you test" philosophy. It is important for them to be able to run the debugger on their AOT-compiled Java applications. Some JVM vendors only permit debugging of interpreted code, so look for AOT debug capability if this is important to you.
There are many other issues and tradeoffs beyond memory footprint, performance, and tools to consider, of course, but I hope this discussion gives you some initial thoughts about whether further investigation into using Java and Linux for your embedded application may be fruitful.
In the next article of this series, I'll talk about some Java-specific issues that every developer needs to be aware of when evaluating JVMs — memory management and determinism.
Return to Table of Contents
Randy Rorden serves as Vice President of Engineering at NewMonics, responsible for development, testing, documentation, and support of the Embedded PERC product family. Randy has 20 years experience developing computer hardware and software products in various markets, including medical management, computer-based training, scientific workstations, multiuser UNIX systems, RAID disk arrays, PC networking, wireless messaging and Internet communications. You may reach him at rrorden@newmonics.com.

