Exploring Java Serviceability Agent (SA): JVM Memory Model, JIT, and Low‑Level Debugging Tools
This article provides a comprehensive, English‑language walkthrough of Java Serviceability Agent (SA), covering how to obtain and analyze JVM internal structures, the memory model and Linux process layout, the oop/klass split, JIT compilation mechanics, and the implementation of tools such as jmap and jstack, all illustrated with code snippets and diagrams.
1 Expected Gains
Master the technique of using SA to probe JVM internals.
Understand the JVM memory model, execution engine, and Linux process memory layout.
Grasp the oop/klass split model of the JVM.
Learn the working principle of JIT and its source‑code analysis.
Analyze the bytecode instruction execution flow.
Explore the implementation of jmap, jstack and other low‑level tools.
Comprehend the overall operation of Java SA.
2 Background
In a micro‑service architecture, rapid business iteration leads to a proliferation of services and code, resulting in bloated systems where many modules become candidates for removal. During the 1024 Programmer's Day hackathon, a "system slimming tool" based on Java SA won the championship by enabling zero‑intrusion detection of removable code. This article records the author's exploration of Java SA, aiming to help readers understand JVM runtime data and internal mechanisms.
3 Source Code Preparation
3.1 Overview of Required Sources
The main HotSpot code is written in C++ with a small amount of C and assembly. Fortunately, SA and other agents are implemented in Java, making source reading easier. The relevant sources are:
<dependency>
<groupId>jdk</groupId>
<artifactId>sa-jdi</artifactId>
<version>1.8</version>
</dependency>Additional tools such as jmap and jstack reside in jdk/src/share/classes/sun/tools/. Object, class, and method representations are in hotspot/src/share/, while Linux process and virtual‑memory structures are in linux-2.6.0/include/linux/.
3.2 Downloading Sources
OpenJDK source: http://hg.openjdk.java.net/jdk8
Linux kernel source: https://mirrors.edge.kernel.org/pub/linux/kernel/v4.x/
4 Java SA Overview
SA (Serviceability Agent) is a private Sun component developed by HotSpot engineers that allows reading Java objects and internal VM data structures from a running Java process or core dump. It differs from the attach mechanism in that SA reads OS‑level memory directly without executing any code inside the target VM.
Execution Mechanism
Direct OS‑level memory read
Impact on Target Process
No
JDK Version Differences
Significant
attach
Client/Server socket interaction; may affect target
Understanding SA requires knowledge of the JVM memory model, the oop/klass split, and JIT compilation.
5 JVM Memory Model and Linux Process Layout
5.1 JVM Memory Model and Execution Engine
Figure 1 (omitted) shows the JVM memory model: shared heap, metaspace, per‑thread Java stack, native stack, and program counter. Execution can be interpreted or compiled, with garbage collection occurring during runtime.
The model abstracts away OS and CPU specifics, providing Java's cross‑platform guarantee. However, the JVM isolates Java processes from the OS, making low‑level inspection challenging—exactly what SA enables.
5.2 Linux Process Memory Layout
In Linux each process is represented by a task_struct. The mm_struct member holds memory information. A simplified excerpt:
struct mm_struct {
struct vm_area_struct *mmap; // linked list of VMA
struct rb_root mm_rb; // red‑black tree of VMA
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
...
}Figure 2 (omitted) illustrates the typical Linux process layout: text segment, data segment, BSS, heap, stack, and memory‑mapped region (shared libraries, etc.). The JVM's libjvm.so is loaded into the memory‑mapped region, and SA reads symbols from this library to locate JVM internal structures.
6 JVM oop/klass Split Model
6.1 Model Overview
All Java objects are represented by two C++ structures: a Klass (metadata) and an OopDesc (instance data). The following example demonstrates the creation of a Student object and the resulting JVM structures.
package com.qunar.sa;
public class TargetProcess {
public static void main(String[] args) throws Exception {
int id = 1;
Student student = new Student();
student.setId(id);
Thread.sleep(10000000L * 1000);
}
static class Student {
private static int type = 10;
private int id;
public void setId(int id) { this.id = id; }
void JITTest1(){ System.out.println(this.id); }
void JITTest2(){ System.out.println(this.id); }
void JITTest3(){ System.out.println(this.id); }
}
}After setId executes, the JVM creates:
A InstanceKlass object (metadata) stored in the metaspace.
An instanceOopDesc object (instance) stored on the heap, with a pointer from the stack variable.
The relevant C++ definition (excerpt from oop.hpp) is:
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;7 JIT Compilation Mechanism
7.1 Method Call Information Storage
Method call counters are stored in the Klass of each class. The counter encodes call count, compilation flag, and state in a 32‑bit integer.
7.2 Hot‑Compilation Triggers
Two triggers exist:
Invocation counter : when the call count exceeds a threshold, the method is compiled asynchronously (standard compilation).
Back‑edge counter : loops generate back‑edge events; once the back‑edge counter exceeds its threshold, OSR (On‑Stack Replacement) compilation occurs.
7.3 Hotness Decay
If a method does not reach the compilation threshold within a certain period, its counter is halved (decay). The decay logic ensures the counter never reaches zero for methods that have been executed at least once.
private: unsigned int _counter; // [count|carry|state]
enum State { wait_for_nothing, wait_for_compile, number_of_states };
inline void InvocationCounter::decay() {
int c = count();
int new_count = c >> 1;
if (c > 0 && new_count == 0) new_count = 1;
set(state(), new_count);
}7.4 Back‑edge Counter Statistics
The bytecode interpreter increments the back‑edge counter on each goto execution. When the OSR threshold is reached, an OSR compilation request is issued.
CASE(_goto): {
int16_t offset = (int16_t)Bytes::get_Java_u2(pc + 1);
address branch_pc = pc;
UPDATE_PC(offset);
DO_BACKEDGE_CHECKS(offset, branch_pc);
CONTINUE;
}
#define DO_BACKEDGE_CHECKS(skip, branch_pc) \
mcs->backedge_counter()->increment(); \
if (do_OSR) do_OSR = mcs->backedge_counter()->reached_InvocationLimit();7.5 Accessing JIT Data via SA (Code Way)
The following simplified SA program collects method invocation counts and compiled‑method information for the Student class.
package com.qunar.sa;
public class SAProcess {
public static void main(String[] args) throws ParseException {
int pid = 20408;
HotSpotAgent agent = new HotSpotAgent();
agent.attach(pid);
try {
Set<MethodDefinition> methodResult = new HashSet<>();
VM.getVM().getSystemDictionary().allClassesDo(new InvocationCounterVisitor(methodResult));
System.out.println("SA method info: " + JacksonSupport.toJson(methodResult));
Set<MethodDefinition> compiledMethodResult = new HashSet<>();
VM.getVM().getCodeCache().iterate(new CompiledMethodVisitor(compiledMethodResult));
System.out.println("SA compiled data: " + JacksonSupport.toJson(compiledMethodResult));
} finally { agent.detach(); }
}
// Visitor implementations omitted for brevity
}Sample output shows invocation counts (after right‑shifting three bits) and which methods have been JIT‑compiled.
8 Implementation of jmap, jstack and Similar Tools
8.1 attach‑Based Mechanism
Tools send a SIGQUIT to the target JVM, which spawns an Attach Listener thread. A socket connection is established, the command (e.g., inspectheap) is sent, the target processes it, and the result is returned over the socket.
8.2 SA‑Based Mechanism
SA reads the target process memory directly, without any cooperation from the target JVM. For jmap -heap, SA locates the global variable gHotSpotVMTypes via the symbol table of libjvm.so, computes its absolute address using the VMA base from /proc/[pid]/maps, and reads the data with ptrace.
9 Java SA Runtime Mechanism
9.1 Steps to Retrieve Data
Identify the offset of a global variable (e.g., gHotSpotVMTypes) from the readelf symbol table.
Obtain the base address of libjvm.so in the target process via /proc/[pid]/maps.
Add offset to base to get the absolute virtual address.
Use ptrace to read the variable's value.
Interpret the raw memory using the VMStructEntry and VMTypeEntry structures, which describe the layout of all JVM objects.
Key structures:
typedef struct {
const char* typeName; // e.g., SystemDictionary
const char* fieldName; // e.g., _dictionary
const char* typeString; // e.g., Dictionary*
int32_t isStatic;
uint64_t offset; // used for non‑static fields
void* address; // absolute address for static fields
} VMStructEntry;
typedef struct {
const char* typeName;
const char* superclassName;
int32_t isOopType;
int32_t isIntegerType;
int32_t isUnsigned;
uint64_t size;
} VMTypeEntry;SA APIs (e.g., sun.JVM.hotspot.HotSpotTypeDataBase) use these entries to locate fields such as SystemDictionary._dictionary and retrieve their values.
References
Usenix paper on Java SA: https://static.usenix.org/event/JVM01/fullpapers/russell/russell.html OpenJDK SA documentation: http://openjdk.java.net/groups/hotspot/docs/Serviceability.html Reading symbol tables from shared libraries: https://blog.csdn.net/raintungli/article/details/7289639
END
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Qunar Tech Salon
Qunar Tech Salon is a learning and exchange platform for Qunar engineers and industry peers. We share cutting-edge technology trends and topics, providing a free platform for mid-to-senior technical professionals to exchange and learn.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
