3.8 Kheiron /JVM: Runtime Adaptation in the Java Virtual Machine
3.8.3 Kheiron /JVM Architecture
The implementation of Kheiron/JVM consists of a JVMTI agent (1890 lines of C++ code) and a set of supporting Java classes that modify classfiles, collect execution statistics and communicate with the JVMTI agent (779 lines of Java code) using the Java Native Interface (JNI). To deploy Kheiron/JVM, the C++ code is packaged in an application extension library, (.dll on Windows and .so on Linux), while the Java code is packaged in a jar file. Figure 3.16shows the five main components of Kheiron/JVM.
• The Kheiron JVMTI Agent receives execution-related events from the JVM. Specif- ically, our agent subscribes to class hook events (e.g., classfile load events for every class), garbage collection events (e.g., GC start and end events), compiled method load events (method compiled, loaded and unloaded events) and exception events (exception thrown and caught events). Our JVMTI agent does not subscribe to method entry/exit events from the JVM since these can severely degrade application perfor- mance [134]. Instead, we rely on the Bytecode Transformer to add method entry and exit profiling hooks to the methods of the classes we are interested in.
Figure 3.16: Kheiron/JVM architecture diagram
• The Bytecode Transformer parses and modifies classfiles. It is able to add new methods or variables to types and add references to other classes or methods. It is also responsible for generating, inserting or replacing the bytecode in existing methods. Bytecode changes can be committed at loadtime (via returning a modified classfile in response to the ClassfileLoadHook event generated by the JVM when a classfile is read from storage) or at runtime via the RedefineClasses function exposed by the JVMTI. In the case of runtime modifications of methods, active method invocations continue to use the old implementation of a method while new invocations use the latest version [134]10. In our Kheiron/JVM implementation the Bytecode Transformer is primarily responsible for injecting instrumentation hooks into classes such that method invocations and object creations can be tracked. The hooks inserted interact with methods exposed by the Stats Collector and/or the Fault Manager depending on the desired adaptation.
• The Stats Collector captures object creation and method entry and exit events via the hooks inserted by the Bytecode Transformer. The Stats Collector uses a number of different book-keeping data structures to manage and collate the events received while
an application is executing.
• The Object Manager manages an object pool of book-keeping data structures. It is responsible for creating, distributing and recycling these book-keeping data-structure instances in an effort to limit the amount of memory consumed by Kheiron related objects.
• The Fault Manager is responsible for injecting faults or inducing failures in a Java application. It relies on hooks inserted by the Bytecode Transformer to capture and/or interact with elements (object instances, data structures, methods implementations, etc.) in the target application.
Communication between the Kheiron JVMTI agent written in C++ and the Kheiron/JVM Java classes is achieved using the Java Native Interface (JNI). JNI is a two-way interface that allows Java code running in a JVM to call and by called by code written in other languages, e.g., C and C++ [132]. Whereas there are other approaches to facilitating communication between Java and non-Java applications, e.g., TCP/IP sockets, interprocess communication mechanisms (IPC), etc., JNI allows communication between Java and non-Java elements that share the same process space as is the case with our JVMTI C++ agent and Kheiron/JVM Java classes, which are hosted in a single JVM process.
The ability of the profiler/JVMTI agent to call or interact with managed (Java) code in a structured way via the JNI APIs is unique to the JVM. This allows JVMTI agents to leverage functionality available in the Java system libraries and/or other Java based libraries. In the CLR, profilers are intended to be purely unmanaged code, i.e., written in C/C++. Profiler developers are warned that “...attempts to combine managed and unmanaged code from a CLR profiler can cause crashes, hangs and deadlocks” [126].
3.8.4
Model of Operation
Kheiron/JVM performs operations on type definitions, object instances and methods at various stages in the execution cycle (see Figure3.17) to make them capable of interacting with an adaptation engine. In particular, to enable an adaptation engine to interact with a class instance, Kheiron/JVM augments the type definition to add the necessary “hooks”. Augmenting the type definition is a two-step operation.
Figure 3.17: First method invocation in the Java HotspotVM
Step 1 occurs at classfile load time (Stage 1 in Figure3.17), signaled by the ClassFileLoad- Hook JVMTI callback that precedes it. At this point the VM has obtained the classfile data from storage but has not yet constructed the in-memory representation of the class. Kheiron/JVM adds what we call shadow methods for each of the original public and/or private methods. A shadow method shares most of the properties – including a subset of attributes, e.g., exception specifications and the method descriptor – of the corresponding original method. However, a shadow method gets a unique name. Figure3.18, transition A to B, shows an example of adding a shadow method SampleMethod for the original
method SampleMethod.
Extending the metadata of a type by adding new methods must be done before the type definition is installed in the JVM. Once a type definition is installed, the JVM will reject the addition or removal of methods. Attempts to call RedefineClasses will fail if new methods or fields are added. Similarly, changing method signatures, method modifiers or inheritance relationships is also not allowed.
Figure 3.18: Preparing and creating a shadow method
Step 2 of type augmentation occurs immediately after the shadow method has been added, while still in the ClassFileLoadHook JVMTI callback. Kheiron/JVM uses bytecode- rewriting techniques to convert the implementation of the original method into a thin wrapperthat calls the shadow method, as shown in Figure3.18, transition B to C.
Kheiron/JVM’s wrappers and shadow methods facilitate the adaptation of class instances. In particular, the regular structure and single return statement of the wrapper method, see Figure3.19, enables Kheiron/JVM to easily inject adaptation instructions into the wrapper as prologues and/or epilogues to shadow method calls.
To add a prologue to a method new bytecode instructions must prefix the existing bytecode instructions. The level of difficulty is the same whether we perform this insertion in the wrapper or in the original method. Adding epilogues, however, is more challenging (as highlighted in 3.7.4). To address these challenges, we employ the same wrapper-based
Figure 3.19: Kheiron/JVM conceptual diagram of a wrapper
approach, which allows us to create a regular method structure with a single entry point, and a single known exit point. The simplified structure of the wrapper makes it easy to add/edit prologues and epilogues as necessary.
To initiate an adaptation, Kheiron/JVM augments the wrapper to insert a jump into an adaptation engine at the control point(s) before and/or after a shadow method call. This allows an adaptation engine to be able to take control before and/or after a method executes. Effecting the jump into the adaptation engine is a two-step process.
• Step 1: Extend the metadata of the classfile currently executing in the JVM such that a reference to the classfile containing the adaptation engine is added to the constant pool11as well as references to the subset of the adaptation engine’s methods that we wish to insert calls to.
• Step 2: Augment the bytecode and metadata of the wrapper function to insert bytecode instructions to transfer control to the adaptation engine before and/or after the existing bytecode that calls the shadow method. The adaptation engine can then perform any number of operations, such as inserting and removing instrumentation, caching class instances, performing consistency checks over class instances and components, injecting faults, or performing reconfigurations and diagnostics of components.
11The constant pool stores symbolic information about fields, methods, interfaces, constants, data types, etc.