Mar 99 Factory Floor
Volume Number: 15
Issue Number: 3
Column Tag: From The Factory Floor
Mac OS Runtime for Java 2.1
by Kelly Jacklin and Dave Mark, ©1999 by Metrowerks, Inc., all rights reserved.
This month, the Factory Floor visits with Kelly Jacklin, a Senior Software Engineer at
Apple working on Java runtime and JIT technology. The newest version of MRJ
(version 2.1) is about to ship, and Kelly was kind enough to take time out of his
schedule to share with us some of the technical details about Apple's virtual machine
for Java. Kelly Jacklin <jacklin@apple.com> has been a software engineer at Apple
for over seven years working on a variety of operating system projects and related
technologies. He began work at Apple on A/UX 3.0, Apple's UNIX operating system, and
was one of the original team members of the Star Trek project to port Mac OS to Intel,
as well as a core microkernel and OS services engineer on the Copland project. He has
spent the last couple of years working on virtual machine and language runtime
technology, as has been involved with all currently shipping versions of RJ. In his
ever decreasing spare time, he works on pointless engineering projects like writing
scheme interpreters and ray tracers in Java. Aside from his engineering and familial
joys of raising his son Kai with his wife Yun, he enjoys playing a plethora of musical
instruments (mostly stringed), much to the chagrin of those around him...
Dave: What exactly is MRJ? Is it simply a VM, or is there more to it
than that?
Kelly: MRJ, or Mac OS Runtime for Java, is Apple's language runtime environment
for the Java programming language. It emulates a virtual machine for interpreting
Java bytecode, supplies the runtime services Java programs expect, and provides
versions of the core Java class libraries that map to Mac OS APIs such as graphics,
networking, file I/O and other OS services.
In addition to these basic services, MRJ also provides developers with the ability to
call the Macintosh toolbox and OS APIs, so they can write Mac applications in Java. The
MRJ SDK includes basic tools for developing Java programs, as well as a powerful
binding tool called JBindery which gives developers an easy way to package their Java
executables as double-clickable Macintosh applications; this capability is also rather
seemlessly integrated into the most recent version of CodeWarrior Pro.
Dave: What's new in MRJ 2.1?
Kelly: MRJ 2.1 reflects a single-minded goal of radically increasing the performance
and stability of the Java platform on the Mac. Previous versions of MRJ, as well as
third-party VMs for the Mac, have been either slow or buggy, or sometimes both.
With 2.1, we strove to correct this trend, and have succeeded in producing a VM that
performs very well, and is very stable. All of the features added in 2.1 reflect this
single-minded approach. Specific areas of improvement include:
• JDK 1.1.6: MRJ 2.1 implements the 1.1.6 version of the Java
environment and APIs. Since we use the shared JDK classes supplied by Sun,
we have picked up many bug fixes from them which help provide stable and
predictable behaviour of the Java APIs.
• AWT: The MRJ implementation of Java's abstract windowing toolkit, has
been completely rewritten, mostly in Java. AWT performance has been
radically enhanced, and this reflects directly in better user-perceived, as
well as actual, performance of most applications. This new AWT
implementation makes extensive use of the Appearance Manager, with the
result that Java applications look very Mac-like when running under MRJ.
This has also been an area in which we have greatly improved our stability and
correctness, where correctness is reflected both in adherence to the Java
specifications for the APIs, as well as in developer expectations where the
specifications are ill-defined.
• Cross-platform Consistency: Many Java developers have been ignoring
the Mac, simply because the Java implementations on the Mac have lacked the
maturity or predictability of behaviour that they have ccome to expect from
Java implementations on Windows. We have geatly enhanced the consistency of
our platform, which allows developers to bring their Java applications over to
the Mac easily and successfully with very little pain. Where specifications of
API behaviour have been ill-defined, we have assessed the behaviour of the
API on other platforms, and gone with the de-facto standard expected by
developers.
• Networking/Security: Through the use of asynchronous networking using
OpenTransport, MRJ 2.1 exhibits greatly enhanced networking performance,
maintaining asynchronous execution of other Java threads while threads
performing network operations remain highly responsive to completion of
outstanding requests. MRJ 2.1 also reflects improved support for networking
and security services, such as RMIC, JDBC, encryption/decryption and code
signing. It is important to note that while there were bugs in some security
features in MRJ 2.0, those bugs manifested in lack of functionality, rather
than security holes.
• SDK: We have improved our support for Java development, by providing
better basic Java development tools, as well as working with third parties to
improve the support for MRJ in their tools. CodeWarrior now has built-in
support for generating executables that use MRJ, and their debugger works
very well with MRJ. In addition, MRJ 2.1 allows Mac developers to write Java
applications that call the Macintosh toolbox and OS APIs, through the use of
wrapper classes automatically generated from the universal Mac OS headers.
• Threading and Synchronization: The low-level VM support in MRJ for
threads and synchronization services has been completely rewritten in 2.1.
This rewrite represents much better performance in areas of
cotext-switching and scheduling, and provides enhanced scheduling support in
areas like priority inheritance for monitors and condition variables. This has
had a direct impact on both benchmark scores and real-world server
applications or computationally intensive code.
• No 68k Support: Radical improvements to a product often come at a price.
In the case of MRJ 2.1, this price was paid in lack of support for 68k
machines. The complexity of the project demanded that we sacrifice support
for this processor, support for which required too much duplicate engineering
and would have caused us to deliver much needed improvements to our
developers much later. Feedback from most of our development community has
shown overwhelming support for, or at least sympathy with, this decision.
• Just-in-time Compiler: One of the more radical enhancements to MRJ has
come with the introduction of a new JITC. For MRJ 2.1, Apple has an
agressively optimizing JITC that far exceeds the performance of our previous
JITC. We have also enhanced this JITC to provide better interaction between
Java code and certain natively-dispatched calls, such as the Macintosh toolbox
APIs. This has dramatically improved performance of Java code running on
our VM in general; which improvement is even more pronounced for
computationally-intensive code or long-running server applications. In
addition, the deployment of this JITC has provided us with the flexibility to
implement much of MRJ itself in Java, providing increased robustness and
feature richness, while still providing competitive performance.
Dave: We hear lot about Just-In-Time compilers. How does a JITC
work? When are methods compiled by a JITC? Are all JITCs strictly
dynamic?
Kelly: Basically, a Java JITC is a dynamic recompilation engine for translating
bytecode into native instructions. The JITC is dynamically loaded during startup of the
Java virtual machine, and plugs into the VM through a set of hooks. When classes are
loaded by the VM, the JITC is notified, and it can decorate the class meta-data with
information it will later need to compile the methods of that class. Implementations of
JITCs vary from simple strict translators to optimizing compilers, and run the gamut
in between. The JITC included in MRJ versions 1.5 and 2.0 performed basic
translation of the bytecode stream into an equivalent native instruction stream, with a
decent register allocator to improve the performance. The advantage of this JITC was
its extremely low cost in compilation, at the expense of runtime performance of the
translated code that paled somewhat in comparison with JITCs in competitive virtual
machines. The new JITC introduced with MRJ 2.1 offers much more compelling
performance of compiled code. However, this performance advantage comes at the cost
of increased compilation time for complex methods. We have mitigated this cost using
some of the mechanisms commonly used in JIT technology.
Compilation is typically performed on first invocation of a method. However, with any
JIT technology other than the most primitive, compilation can be computationally
expensive for complex methods. For this reason, it is often better to defer compilation
of a method until it is known that the speed increase of the compiled code would make
up for the cost of compilation. This can be done by applying heuristics to allow a
method to be interpreted until it is determined to be frequently called, at which time it
is compiled. A JITC can also make use of the Java VM's threading services to defer
compilation to a lower-priority background thread. In these ways, the cost of
optimizing compilation can be amortized or deferred so that it is not noticable to
time-critical or user-interface code.
It is also possible for a JITC to cache the results of compilation for use by a subsequent
execution. However, due to the dynamic nature of the Java language, this typically
involves bloating the code, or having complex fixup data stored with the code, so that
dynamic conditions can be updated when the code is restored. Such conditions include,
for example, whether dependent classes have been statically initialized or not, as well
as things like the constants values for field offset and method table offsets that can
vary as classes upon which the target class is dependent are changed. A JITC that caches
compiled code is, by its nature, not strictly dynamic.
Dave: What kinds of optimizations can a JITC perform? And are there
specific coding techniques I can use to make my code more JITC
friendly?
Kelly: A JITC can perform any optimization that maintains the correct behaviour of
the Java code, as defined by the specifications. This includes traditional optimizations,
such as various loop transformations, common sub-expression elimination, global
register allocation, dead code elimination, copy propogation, etc. In addition, the Java
language lends itself well to several OO-centric optimizations, such as monomorphic
dispatch optimization and method inlining. Since, in Java, the majority of instance
methods require virtual dispatch, optimizing virtual dispatch to methods that can be
proven to have only one current implementation can be a big win. Most JITCs do this
unconditionally for dispatches to any methods marked with the "final" keyword, or any
methods belonging to classes marked with that keyword, so appropriate use of this
keyword can improve performance. One must be careful, however, not to
unnecessarily restrict code with aggressive use of the "final" keyword, as this
keyword makes sub-classing difficult or impossible, thus sacrificing flexibility in the
usage model of the class.
In general though, developers should not be too cognizant of the presense of a JITC,
other than the fact that their code performs well. The JITC in MRJ 2.1 performs
optimizations in places that would surprise the average developer, and which cannot be
explicitly coded for in advance. Developers should trust the JITC to do its job, and not
try to second-guess it in their code. In some cases, attempts that a developer makes to
anticipate optimization will backfire. Some tools that "optimize" the Java bytecode in a
class, for example, would defeat later optimization efforts by the JITC that shipped
with MRJ 2.0.
Dave: Once the JITC has generated PPC code, how does that code interface
with the VM?
Kelly: When the JITC starts up, the VM establishes a set of callbacks into the VM for
the JITC to use, both during compilation and during runtime. Information is stored by
the JITC in the meta-data for a method which allows the VM to invoke the generated
native code for that method. Similarily, there is information in the meta-data for
methods that have not been compiled on how to invoke that method using the VM. The
transitions are handled through data-driven dispatching bottlenecks. These bottlenecks
facilitate bi-directional "mode switches" between Java and compiled native code,
similar in concept to the co-existence of PPC and 68k emulated code under MacOS.
In addition, both the JITC and VM provide a number of "helper" functions that can be
called by compiled code. This includes code to perform such operations as object
allocation, math routines, Java exception processing, and transitions out of Java into
statically compiled C/C++ code. These routines are made available to the compiled code
through linkages established at compile time.
Dave: What facilities exist to allow programmers to mix C/C++ and
Java within a single program?
Kelly: Java supports calling C/C++ code from Java, as well as invoking Java from
C/C++. The facilities in C/C++ that allow this are collectively termed the Java Native
Interface (JNI) APIs. Calling C/C++ code from Java traditionally assumes that the
C/C++ code is aware that it is being called from Java, which has required developers
to write bridging code to map Java interfaces onto their existing or legacy code. This
has been a problem for many developers. To help ease this integration problem, as
well as ease our own internal development of MRJ itself, MRJ also supports a
proprietary native code invocation model called JDirect, as well as the traditional
Java-supported mechanisms. JDirect (not to be confused with J/Direct, which is a
Microsoft technology) allows Java code to invoke native shared-library-based
routines directly, without the need for wrapper code written in C/C++. It takes care
of looking up the routine by symbol from the appropriate shared library, and
marshalling parameters from Java into the native calling conventions and calling the
target routine. This technology forms the basis for access to the Mac OS APIs provided
by MRJ 2.1.
A closely related topic to the invocation integration between native and Java code is the
issue of embedding Java content within the windows of a Mac application. MRJ 2.1
provides an embedding API, called JManager2, which allows applications to host Java
content inside of their application windows, or in separate windows devoted to Java
content. It provides APIs to manage window real estate, as well as to delegate events and
give time to Java code running within the application. For example, JManager is used
to embed Java content inside web browser windows in Microsoft Internet Explorer.
Using these APIs and services, developers can integrate native code with Java code all
in the same application. This gives developers the flexibility to write portions of their
application in Java, or to use Java as a plug-in technology to allow their application
functionality to be extended by third parties in a cross-platform and compatible
fashion.