This is the first in a two-part series on the benefits of using the Java development and runtime environment for embedded computing. Java, with its "write once, run anywhere" paradigm, is ideal for embedded computing because of its portability, reliability, security, and Internet capabilities.
However, Java can present some challenges for embedded-systems developers. First, speed - Java applications are inherently slower than applications compiled into native machine code and embedded processors are generally less powerful than those found on desktops. There's no direct memory access and no interrupt handling, two features usually required for developing low-level software to control hardware, and Java can't easily run in real time, to name only a few of the issues.
Despite its limitations, Java can be used effectively for embedded systems. In this article, I share Wind River's experience in embedded real-time computing to help developers overcome some of the common pitfalls inherent in the Java development environment. Part 2 will help you determine if Java is right for your embedded development project.
Java for the Embedded World
Unlike desktop systems, embedded systems use different user interface technologies; have significantly smaller memories and screen sizes; use a wide variety of embedded processors; and have tight constraints on power consumption, user response time, and physical space. The original Java Developer's Kit (JDK) technology was designed for desktop environments with powerful processors, large disks, and large available memory spaces. Consequently, the full JDK architecture is not suitable for many applications in the embedded world.
However, Sun has also introduced versions of the first iteration of Java (JDK 1.1 or Java 1) for the embedded world, namely EmbeddedJava and PersonalJava. The newest iteration, SDK 1.2 and higher (1.3, 1.4) or Java 2, is grouped into three editions, one of which is the Java 2 Micro Edition (J2ME) aimed at the consumer electronics and embedded market. EmbeddedJava, PersonalJava, and J2ME provide standard, platform-independent Java development environments that reduce costs and shrink development cycles for Java applications and applets running on embedded devices.
EmbeddedJava is a scalable and configurable environment suitable for low-end embedded devices with dedicated functionality and limited memory. It's ideal for closed system devices that don't require Web browsing capabilities and don't expose application programming interfaces (APIs) to the outside world. EmbeddedJava includes tools that allow developers to configure and compile runtime environments that contain only those fields and methods necessary for a particular application's needs.
An executable image of a complete EmbeddedJava environment can be generated and placed in the embedded system's ROM. A dedicated tool chain creates optimized application executables known as ROMlets, which can be programmed into the device's ROM, and patchlets, enhanced ROMlets that can be upgraded in the field. Developers can use EmbeddedJava for a variety of products, including process controllers, instrumentation, office printers and peripherals, and networking routers and switches.
PersonalJava is an upward-compatible subset of Java dedicated to consumer and embedded devices, and specifically designed for building network-connectable consumer devices for home, office, and mobile use. It consists of the JVM and a subset of the JDK 1.1 APIs, including core and optional APIs and class libraries. PersonalJava includes the specific tools and APIs required by embedded applications in resource-limited environments. Examples of devices suitable for the PersonalJava application environment include mobile handheld devices, set-top boxes, game consoles, and smartphones.
J2ME, designed for the development of such devices as digital cellular phones, pagers, personal digital assistants, digital set-top boxes, and retail payment terminals, defines vertical platforms called profiles that sit on top of two different configurations.
The connected device configuration (CDC) uses a 32-bit standard JVM and requires more than 2MB of memory. This configuration relies on some kind of connection to a network and on an underlying RTOS and C runtime environment. The connected limited device configuration (CLDC) uses the 16- or 32-bit KVM and requires 256-512KB of memory. It doesn't necessarily require a "persistent" network connection. Profiles within these configurations are integrated into the J2ME framework with each profile targeting a precise vertical market.
As discussed, some mechanisms of the JVM and Java as defined in the desktop-oriented platforms (JDK 1.1 or Java 2) are not suitable for embedded systems. PersonalJava, EmbeddedJava, and J2ME define a framework for optimized embedded Java implementations, but these Java versions alone are not ideal unless coupled with some background and experience in the development of real-time and embedded applications. The key to success is in knowing how to architect Java technology specifically for an embedded device.
Java is a semicompiled language and therefore inherently much slower than native machine-code execution. This speed issue is exacerbated by the fact that embedded processors are usually less powerful than desktop ones. The easiest and most expensive way to enhance Java performance is to use a faster processor. Fortunately, there are other solutions.
Just-In-Time (JIT) compilers for desktop JVMs accelerate the Java interpretation cycle by translating Java byte code into machine code on the fly. In their original form, JIT compilers are not suitable for use with embedded applications because they require a lot of dynamic memory. The compilers are also unable to reach the performance of traditionally compiled C/C++ code because they translate Java byte code into native code at runtime as opposed to buildtime. Developers can achieve a tradeoff between performance and memory footprint with dynamic adaptive compilers - JIT compilers customized for embedded applications - that perform statistical analysis of byte code prior to its translation into native machine code.
Flash (or "pass-through" JIT) compilers aren't embedded in the JVM like JIT compilers, instead they run separately on a network host as "compiling" servers. Upon a class download demand, the flash compiler compiles the requested Java bytecode and passes the resulting native code to the JVM. Flash compilers are still runtime compilers and as such, they'll slow runtime execution to some extent. Plus, they deliver classes over a network during application execution, which can also slow execution speed.
Ahead-of-time compilation translates Java source code to C code (losing the Java portability) or translates Java byte code to native machine code (retaining portability at the application level). Unlike JIT or flash compilers, ahead-of-time compilers work at compile-time and can achieve optimizations similar to those achieved by traditional compilers. Developers can avoid undesirable code expansion due to byte code-to-machine-code translation by compiling 20% of the most relevant Java code into native machine code, with the JVM interpreting the remaining 80%.
Executing Code from ROM
Desktop JVMs usually can't execute Java code directly from ROM. Normally, Java classes are first loaded into RAM, verified, and then executed by the JVM. This approach is impractical for many embedded systems because it increases the use of expensive RAM beyond the cost constraints of the embedded system.
A ROMizer, such as Java CodeCompact for PersonalJava and EmbeddedJava, creates ROM-based executable images of Java classes. To execute Java code out of ROM instead of RAM, a ROMizer utility processes Java class files into a runtime format that can be run directly out of ROM or flash memory by the JVM. The ROMizing process frees the JVM from the class-file loading and byte code-verification phases, and improves the performance and start-up time of Java applications.
Direct Access to Memory/Files
Java includes a large set of classes that are not scalable by default. Desktop JVMs usually require several megabytes of disk space and RAM to execute. Embedded systems usually don't have large disk drives or big memory spaces, although some of this memory usage may be converted into ROM/flash memory for diskless embedded systems. Therefore, it's critically important that developers tailor the operating system and runtime components for embedded systems to each application to avoid unnecessary memory usage.
However, it may not be easy to analyze and remove all unused classes and methods from Java to minimize the embedded application's final memory footprint. Desktop JVMs usually download class files from a local hard drive or a file system on the network, and these resources aren't always available to embedded systems.
The best scalability that can be achieved starts with the underlying real-time operating system (RTOS) scalability and can be tracked at three different levels on the Java side. First, the JVM may be scalable depending on the services the application requires. Second, the Java classes may also be sorted and only included in the runtime system if they're used by the application. The verbose option of the Java launcher can be helpful in the analysis of the used classes. Finally, a dedicated utility tool, such as EmbeddedJava's Java Filter, can be used to skim Java classes and remove unused methods and fields.
Developers may want to avoid loading Java class files from a local or networked file system, or use them only as an option. They can either be stored in a memory-based virtual file system (RAM, ROM, or flash) or can be converted with a "file-izer" utility tool into a C data structure that stores the class file byte-code content that's linked to the RTOS image.
Running Java in Real Time
Java's biggest problem with real-time execution is that Java garbage-collection algorithms are usually nondeterministic. Once garbage collection starts, it must run to completion and can't be preempted by a more urgent Java thread. The time required for the garbage collector to run is not predictable. This characteristic creates unbounded latency in event response, a condition that can't be allowed in real-time systems. However, even a deterministic garbage collector may not guarantee a real-time execution of Java programs.
There are currently no real-time Java implementations compatible with standard Java platform specifications, even though a consortium of companies, including Wind River, have been working with Sun to define standard real-time extensions to Java under the Java Community Process (in the JSR-00001, see www.rtj.org). The best existing solution to Java's real-time shortcomings consists of a hybrid solution. Developers can write the real-time part of an application in C/C++ for the targeted RTOS, and the rest in Java using JNI to connect the two worlds.
Developers must pay careful attention to the implementation of the JVM on top of the RTOS, making sure the overall system operates in real time even when Java is running. In addition, when evaluating JVM solutions, developers must consider memory management, garbage collection and object finalization, multithreading, interthread synchronization, networking, and graphics.
Java is suited for embedded applications because it can help developers leverage business from a new perspective, especially within the Internet-access device market where connectivity, interactivity, reliability, security, and dynamic extensibility are vital requirements. Embedded devices usually have severe memory and power consumption constraints, lower processor power, real-time behavior, and other requirements that Java doesn't usually handle well. Numerous companies offer leadership products and solutions that can help developers make Java work in embedded solutions.
Vincent Perrier is Wind River's product manager for Java platforms. He has a computer science and engineering degree from the University of Nantes in France.