All Categories :
Java
Chapter 44
Just-In-Time Compilers
by Michael Morrison
CONTENTS
Java programs have been criticized from early on because of their
relatively slow execution speeds. Admittedly, compared to natively
compiled programs written in languages like C/C++ or Pascal, Java
programs are pretty sluggish. However, this complaint has to be
weighed heavily against the inherently cross-platform nature of
Java, which simply isn't possible with native programs such as
those generated by C/C++ and Pascal compilers. In an attempt to
alleviate the inherent performance problems associated with processor-independent
Java executables, various companies are offering just-in-time
(JIT) Java compilers, which compile Java bytecode executables
into native programs just before execution.
This chapter explores JIT compilers and how they impact the overall
landscape of Java. You learn all about the Java virtual machine
and how JIT compilers fit into its organization. Furthermore,
you learn about specific types of Java programs that benefit the
most from JIT compilation. By the end of this chapter, you'll
have a better understanding of this exciting new technology and
how it can improve the performance of your own Java programs.
To fully understand what a just-in-time (JIT) compiler is and
how it fits into the Java runtime system, you must have a solid
understanding of the Java virtual machine (VM). The Java VM is
a software abstraction for a generic hardware platform and is
the primary component of the Java system responsible for portability.
The purpose of the VM is to allow Java programs to compile to
a uniform executable format, as defined by the VM, which can be
run on any platform. Java programs execute within the VM itself,
and the VM is responsible for managing all the details of actually
carrying out platform-specific functions.
When you compile a Java program, it is compiled to be executed
under the VM. Contrast this to C/C++ programs, which are compiled
to be run on a real (nonvirtual) hardware platform, such as a
Pentium processor running Windows 95. The VM itself has characteristics
very much like a physical microprocessor, but it is entirely a
software construct. You can think of the VM as an intermediary
between Java programs and the underlying hardware platform under
which all programs must eventually execute.
Even with the VM, at some point, all Java programs must be resolved
to a particular under-lying hardware platform. In Java, this resolution
occurs within each particular VM implementation. The way this
works is that Java programs make calls to the VM, which in turn
routes them to appropriate native calls on the underlying platform.
Knowing this, it's fairly obvious that the VM itself is highly
platform dependent. In other words, each different hardware platform
or operating system must have a unique VM implementation that
routes the generic VM calls to appropriate underlying native services.
Because a VM must be developed for each different platform, it
is imperative that it be as lean as possible. Another benefit
of having a compact VM is the ability to execute Java programs
on systems with fewer resources than desktop computer systems.
For example, JavaSoft has plans to use Java in consumer electronics
devices such as televisions and cellular phones. A compact, efficient
VM is an essential requirement in making Java programs run in
highly constrained environments such as these.
Just as all microprocessors have instruction sets that define
the operations they can perform, so does the Java VM. VM instructions
compile into a format known as bytecodes, which is the
executable format for Java programs that can be run under the
VM. You can think of bytecodes as the machine language for the
VM. It makes sense, then, that the JDK compiler generates bytecode
executables from Java source files. These bytecode executables
are always stored as .class
files. Figure 44.1 shows the role of the VM in the context of
the Java environment.
Figure 44.1: The role of the VM in the Java environment.
In Figure 44.1, notice how the VM is nestled within the Java runtime
system. It is through the VM that executable bytecode Java classes
are executed and ultimately routed to appropriate native system
calls. A Java program executing within the VM is executed a bytecode
at a time. With each bytecode instruction, one or more underlying
native system calls may be made by the VM to achieve the desired
result. In this way, the VM is completely responsible for handling
the routing of generic Java bytecodes to platform-specific code
that actually carries out a particular function. The VM has an
enormous responsibility and is really the backbone of the entire
Java runtime environment. For more gory details about the inner
workings of the VM, refer to Chapter 34,
"Java Under the Hood: Inside the Virtual Machine."
JIT compilers alter the role of the VM a little by directly compiling
Java bytecode into native platform code, thereby relieving the
VM of its need to manually call underlying native system services.
The purpose of JIT compilers, however, isn't to allow the VM to
relax. By compiling bytecodes into native code, execution speed
can be greatly improved because the native code can be executed
directly on the underlying platform. This stands in sharp contrast
to the VM's approach of interpreting bytecodes and manually making
calls to the underlying platform. Figure 44.2 shows how a JIT
compiler alters the role of the VM in the Java environment.
Figure 44.2: The role of the VM and JIT compiler in the Java environment.
Notice that instead of the VM calling the underlying native operating
system, it calls the JIT compiler. The JIT compiler in turn generates
native code that can be passed on to the native operating system
for execution. The primary benefit of this arrangement is that
the JIT compiler is completely transparent to everything except
the VM. The really neat thing is that a JIT compiler can be integrated
into a system without any other part of the Java runtime system
being affected. Furthermore, users don't have to fool with any
configuration options; their only clue that a JIT compiler is
even installed may simply be the improved execution speed of Java
programs.
The integration of JIT compilers at the VM level makes JIT compilers
a legitimate example of component software; you can simply plug
in a JIT compiler and reap the benefits with no other work or
side effects.
Even though JIT compiler integration with the Java runtime system
may be transparent to everything outside the VM, you're probably
thinking that there are some tricky things going on inside the
VM. In fact, the approach used to connect JIT compilers to the
VM internally is surprisingly straightforward. In this section,
I describe the inner workings of Borland's AppAccelerator JIT
compiler, which is the JIT compiler used in Netscape Navigator
3.0. Although other JIT compilers, such as Microsoft's JIT compiler
in Internet Explorer, may differ in some ways, they ultimately
must tackle the same problems. By understanding Borland's approach
with AppAccelerator, you gain insight into the implementation
of JIT compilers in general.
The best place to start describing the inner workings of the AppAccelerator
JIT compiler is to quickly look at how Java programs are executed
without a JIT compiler. A Java class that has been loaded
into memory by the VM contains a V-table (virtual table), which
is a list of the addresses for all the methods in the class. The
VM uses the V-table whenever it has to make a call to a particular
method. Each address in the V-table points to the executable bytecode
for the particular method. Figure 44.3 shows what the physical
V-table layout for a Java class looks like.
Figure 44.3: The physical V-table layout for a Java class.
Note |
The term V-table is borrowed from C++, where it stands for virtual table. In C++, V-tables are attached to classes that have virtual methods, which are methods that can be over-ridden in derived classes. In Java, all methods are virtual, so all classes have V-tables.
|
When a JIT compiler is first loaded, the VM pulls a little trick
with the V-table to make sure that methods are compiled into native
code rather than executed. What happens is that
each bytecode address in the V-table is replaced with the address
of the JIT compiler itself. Figure 44.4 shows how the bytecode
addresses are replaced with the JIT compiler address in the V-table.
Figure 44.4: The physical V-table layout for a Java class with a JIT compiler present.
When the VM calls a method through the address in the V-table,
the JIT compiler is executed instead. The JIT compiler steps in
and compiles the Java bytecode into native code and then patches
the native code address back to the V-table. From now on, each
call to the method results in a call to the native version. Figure
44.5 shows the V-table with the last method JIT compiled.
Figure 44.5: The physical V-table layout for a Java class with one JIT-compiled method.
One interesting aspect of this approach to JIT compilation is
that it is performed on a method-by-method basis. In other words,
the compilation is performed on individual methods, as opposed
to entire classes. This is very different from what most of us
think of in terms of traditional compilation. Just remember that
JIT compilation is anything but traditional!
Another added benefit of the method-by-method approach to compilation
is that methods are compiled only when they are called. The first
time a method is called, it is compiled; sub-
sequent calls result in the native code being executed. This approach
results in only the methods that are actually used being compiled,
which can yield huge performance benefits. Consider the case of
a class in which only four out of ten methods are being called.
The JIT compiler compiles only the four methods called, resulting
in a 60-percent savings in compile time (assuming that the compile
time for each of the methods is roughly the same).
Just in case you're worried about the original bytecode once a
method has been JIT compiled, don't worry, it's not lost. To be
honest, I didn't completely tell the truth about how the
V-table stores method address information. What really happens
is that each method has two V-table entries, one for the bytecode
and one for the native code. The native code address is the one
that is actually set to the JIT compiler's address. This V-table
arrangement is shown in Figure 44.6.
Figure 44.6: The physical V-table layout for a Java class with both bytecode and native code entries.
The purpose of having both bytecode and native code entries in
the V-table is to allow you to switch between which one is executed.
In this way, you can simultaneously execute some methods as bytecode
and some using JIT-compiled native code. It isn't immediately
apparent what benefits this arrangement will have, but the option
of conditionally using the JIT compiler at the method level is
something that may come in handy.
Okay, so the JIT compiler is integrated with the VM primarily
through the V-table for each class loaded into memory. That's
fine, but how is the JIT compiler installed and recognized by
the VM in the first place? When the VM is first loaded, it looks
for the JIT compiler and loads it if it is found. After loading,
the JIT compiler installs itself by hooking into the VM and modifying
the class-loading mechanism to reference the compiler. From this
point on, the VM doesn't know or care about the compiler. When
a class is loaded, the compiler is notified through its hook to
the VM and the V-table trickery is carried out.
You may have some concerns about how JIT compilers impact the
security of Java programs-because they seem to have a lot of say
over what gets executed and how. You'll be glad to know that JIT
compilers alter the security landscape of Java very little. The
reason is that JIT compilation is performed as the last stage
of execution, after the bytecode has been fully checked by the
runtime system. This is very important because native code can't
be checked for security breaches like Java bytecode can be. So
it is imperative that JIT compilation occur on bytecode that has
already been security checked by the runtime system.
It is equally important that native code (code that has been JIT
compiled) is executed directly from memory and isn't cached on
a local file system to be executed later. Again, doing so would
violate the whole idea of checking every Java program immediately
before execution. Actually, this approach wouldn't qualify as
JIT compilation anyway, because the code wouldn't really be compiled
just in time.
In terms of security, the cleanest and safest approach to JIT
compilation is to compile bytecode directly to memory and throw
it away when it is no longer needed. Because native code is disposable
in this scenario, it is important that it can be quickly recompiled
from the original bytecode. This is where the approach of compiling
only methods as they are called really shines.
None of the details surrounding JIT compilers would really matter
if they didn't perform their job and speed up the execution speed
of Java programs. The whole point of JIT compilation is to realize
a performance gain by compiling VM bytecode to native code at
runtime. Knowing this, let's take a look at just how much of a
performance improvement JIT compilers provide.
In assessing JIT compiler performance, it's important to understand
exactly where performance gains are made. One common misconception
surrounding JIT compilation is the amount of code affected. For
example, if a particular JIT compiler improves the execution speed
of bytecode by an order of ten (on average), then it seems only
logical that a Java program executing under this JIT compiler
would run ten times faster. However, this isn't the case. The
reason is that many programs, especially applets, rely heavily
on the Java AWT, which on the Windows platform is written entirely
in native C. Because the AWT is already written in native code,
programs that rely heavily on the AWT don't reap the same performance
gains as programs that depend on pure Java bytecode. A heavily
graphical program that makes great use of the AWT may see performance
gains by an order of only two or three.
On the other hand, a heavily processing-intensive Java program
that uses lots of floating-point math may see performance gains
closer to an order of fifteen. This happens because native Pentium
code on a Windows machine is very efficient with floating-point
math. Of course, other platforms may differ in this regard. Nevertheless,
this will probably remain a common theme across all platforms:
nongraphical programs are less affected by JIT compilation than
computationally intensive programs.
Now that I've tempered your enthusiasm a little for how greatly
JIT compilation impacts performance, let's look at some hard numbers
that show the differences between interpreted and JIT-compiled
code across different JIT compiler implementations. Figure 44.7
shows a graph of Netscape Navigator 3.0's performance benchmarks
for various JIT-compiled Java operations as measured using Pendragon
Software's CaffeineMark 2.01 benchmark suite.
Figure 44.7: Performance benchmarks for various JIT-compiled Java operations in Netscape Navigator 3.0.
In looking at Figure 44.7, you may be wondering exactly what the
numbers mean. The numbers show the relative performance of Netscape
Nagivator as compared to the Symantec Café applet viewer
running in debug mode. The Café applet viewer produces
scores of exactly 100 on all benchmark tests, so scores above
100 represent a higher browser execution speed than the Café
applet viewer. Likewise, scores lower than 100 represent slower
browser execution. You can see that, in some areas, Navigator
blows away the Café applet viewer with scores in the thousands.
In other areas, however, the JIT-compiled Navigator code slips
a little and is actually slower than the interpreted code. Most
of these areas are related to graphics operations, which highlights
the fact that Café has more efficient graphics support
than Navigator.
Figure 44.8 shows the results of running the same benchmark tests
on the JIT compiler in Microsoft Internet Explorer 3.0.
Figure 44.8: Performance benchmarks for various JIT-compiled Java operations in Microsoft Internet Explorer 3.0
It's interesting to note that Internet Explorer outperformed Navigator
on all tests except one. This is expected because Microsoft claims
to have the fastest Java implementation around. Even so, this
is still the first round of support for JIT compilers, so expect
to see plenty of competition in the future between the JIT compiler
implementations in different browsers. Marketing hype aside, you
can see from these figures that JIT compilation improves performance
significantly in many areas regardless of your browser of choice.
Navigator and Internet Explorer both show an overall performance
improvement that is over eleven times faster than Café's
interpreted approach. Just remember that this improvement depends
largely on the type of applet you are running and whether it is
processing or graphics intensive.
In this chapter, you learned about just-in-time (JIT) compilers
and how they impact the Java runtime system. You began the chapter
by peering into the runtime system to see exactly where JIT compilers
fit in. In doing so, you learned a great deal about the Java virtual
machine (VM), which is largely responsible for the integration
of JIT compilers into the runtime system. Once you gained an understanding
of how JIT compilers relate to the VM, you moved on to learning
about the details of a particular JIT compiler implementation.
This look into the inner workings of a real JIT compiler helped
give you insight into what exactly a JIT compiler does.
Even though the technical details of JIT compilers are important
to understand, little of it would be meaningful if JIT compilers
didn't deliver on their promise to improve Java execution speed.
For this reason, you spent the last part of the chapter learning
about the specific areas where JIT compilers improve Java performance.
Furthermore, you saw benchmark tests comparing JIT compiler performance
in the two most popular Web browsers available. Through these
benchmark tests, you were able to get an idea of how dramatically
JIT compilation can improve performance.
Contact
reference@developer.com with questions or comments.
Copyright 1998
EarthWeb Inc., All rights reserved.
PLEASE READ THE ACCEPTABLE USAGE STATEMENT.
Copyright 1998 Macmillan Computer Publishing. All rights reserved.