1
This article is authorized to collate and publish by the chief lecturer of HeapDump performance community Kumo (Ma Zhi)

Part 1-About the Java Virtual Machine HotSpot, the beginning is simple

Let's talk about the Java runtime, this article will talk about some simple content. How is the main() method in the main class we wrote to be called by the Java virtual machine? Some methods in the Java class will be called by the C/C++ functions of the HotSpot virtual machine written in C/C++. However, because the calling conventions of Java methods and C/C++ functions are different, they cannot be called directly. JavaCalls::call is required. () This function assists in calling. (I call functions written in C/C++, and methods written in Java, and I will continue to use this name in the future) as shown in the figure below.

Some Java methods called from C/C++ functions mainly include:

(1) The main() method in the Java main class;

(2) When the Java main class is loaded, call the JavaCalls::call() function to execute the checkAndLoadMain() method;

(3) In the process of class initialization, call the Java class initialization method executed by the JavaCalls::call() function, you can view the JavaCalls::call_default_constructor() function, which has the calling logic for the method;

(4) Let's omit the execution process of the main method first (in fact, the execution of the main method is to start a JavaMain thread first, and the routine is the same), just look at the startup process of a certain JavaThread. The start of JavaThread is ultimately done through a native method java.lang.Thread#start0() method, which passes through the native_entry entry of the interpreter and calls the JVM_StartThread() function. The static void thread_entry(JavaThread* thread, TRAPS) function will call the JavaCalls::call_virtual() function. JavaThread will eventually call the run() method in the bytecode through the JavaCalls::call_virtual() function;

(5) In SystemDictionary::load_instance_class(), a function that can reflect the delegation of parents, if the class loader object is not empty, the loadClass() function of this class loader (called by the call_virtual() function) will be called. Load the class.

Of course, there are other methods, which are not listed here. Call Java methods through JavaCalls::call(), JavaCalls::call_helper() and other functions. These functions are defined in the JavaCalls class. The definition of this class is as follows:

Some Java methods called from C/C++ functions mainly include:

(1) The main() method in the Java main class;

(2) When the Java main class is loaded, call the JavaCalls::call() function to execute the checkAndLoadMain() method;

(3) In the process of class initialization, call the Java class initialization method executed by the JavaCalls::call() function, you can view the JavaCalls::call_default_constructor() function, which has the calling logic for the method;

(4) Let's omit the execution process of the main method first (in fact, the execution of the main method is to start a JavaMain thread first, and the routine is the same), just look at the startup process of a certain JavaThread. The start of JavaThread is ultimately completed through a native method java.lang.Thread#start0() method, which passes through the native_entry entry of the interpreter and calls the JVM_StartThread() function. The static void thread_entry(JavaThread* thread, TRAPS) function will call the JavaCalls::call_virtual() function. JavaThread will eventually call the run() method in the bytecode through the JavaCalls::call_virtual() function;

(5) In SystemDictionary::load_instance_class(), a function that can reflect the delegation of parents, if the class loader object is not empty, the loadClass() function of this class loader (called by the call_virtual() function) will be called. Load the class.

Of course, there are other methods, which are not listed here. Call Java methods through JavaCalls::call(), JavaCalls::call_helper() and other functions. These functions are defined in the JavaCalls class. The definition of this class is as follows:

Source code location: openjdk/hotspot/src/share/vm/runtime/javaCalls.hpp

class JavaCalls: AllStatic {
  static void call_helper(JavaValue* result, methodHandle* method, JavaCallArguments* args, TRAPS);
 public:

  static void call_default_constructor(JavaThread* thread, methodHandle method, Handle receiver, TRAPS);

  // 使用如下函数调用Java中一些特殊的方法,如类初始化方法<clinit>等
  // receiver表示方法的接收者,如A.main()调用中,A就是方法的接收者
  static void call_special(JavaValue* result, KlassHandle klass, Symbol* name,Symbol* signature, JavaCallArguments* args, TRAPS);
  static void call_special(JavaValue* result, Handle receiver, KlassHandle klass,Symbol* name, Symbol* signature, TRAPS); 
  static void call_special(JavaValue* result, Handle receiver, KlassHandle klass,Symbol* name, Symbol* signature, Handle arg1, TRAPS);
  static void call_special(JavaValue* result, Handle receiver, KlassHandle klass,Symbol* name, Symbol* signature, Handle arg1, Handle arg2, TRAPS);

  // 使用如下函数调用动态分派的一些方法
  static void call_virtual(JavaValue* result, KlassHandle spec_klass, Symbol* name,Symbol* signature, JavaCallArguments* args, TRAPS);
  static void call_virtual(JavaValue* result, Handle receiver, KlassHandle spec_klass,Symbol* name, Symbol* signature, TRAPS); 
  static void call_virtual(JavaValue* result, Handle receiver, KlassHandle spec_klass,Symbol* name, Symbol* signature, Handle arg1, TRAPS);
  static void call_virtual(JavaValue* result, Handle receiver, KlassHandle spec_klass,Symbol* name, Symbol* signature, Handle arg1, Handle arg2, TRAPS);

  // 使用如下函数调用Java静态方法
  static void call_static(JavaValue* result, KlassHandle klass,Symbol* name, Symbol* signature, JavaCallArguments* args, TRAPS);
   static void call_static(JavaValue* result, KlassHandle klass,Symbol* name, Symbol* signature, TRAPS);
  static void call_static(JavaValue* result, KlassHandle klass,Symbol* name, Symbol* signature, Handle arg1, TRAPS);
  static void call_static(JavaValue* result, KlassHandle klass,Symbol* name, Symbol* signature, Handle arg1, Handle arg2, TRAPS);

  // 更低一层的接口,如上的一些函数可能会最终调用到如下这个函数
  static void call(JavaValue* result, methodHandle method, JavaCallArguments* args, TRAPS);
};

The above functions are all self-explanatory, and we can see the role of these functions by their names. Among them, the JavaCalls::call() function is a lower-level general interface. There are 5 bytecode instructions defined by the Java Virtual Machine Specification, which are invokestatic, invokedynamic, invokestatic, invokespecial, and invokevirtual method invocation instructions. These call_static() and call_virtual() functions internally call the call() function. In this section, we will not introduce the specific implementation of each method. The next article will introduce it in detail.

We choose an important main() method to view the specific call logic. The following is basically copying the contents of R, but I made some modifications, as follows:

Assuming that the class name of our Java main class is JavaMainClass, in order to distinguish between main() in C/C++ in java launcher and main() in Java layer program, write the latter as JavaMainClass.main() method.
Starting from the main() function that just entered C/C++:

The main logic of the thread execution that starts and calls the main() function of the HotSpot virtual machine is as follows:

main()
-> //... 做一些参数检查
-> //... 开启新线程作为main线程,让它从JavaMain()函数开始执行;该线程等待main线程执行结束

In the above thread, another thread will be started to execute the JavaMain() function, as follows:

JavaMain()
-> //... 找到指定的JVM
-> //... 加载并初始化JVM
-> //... 根据Main-Class指定的类名加载JavaMainClass
-> //... 在JavaMainClass类里找到名为"main"的方法,签名为"([Ljava/lang/String;)V",修饰符是public的静态方法
-> (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs); // 通过JNI调用JavaMainClass.main()方法

The above steps are still under the control of the java launcher; when the control is transferred to the JavaMainClass.main() method, there will be no java launcher. After the JavaMainClass.main() method returns, the java launcher takes over to clean up and close the JVM.

Let's take a look at the main methods that will go through when calling the main() method of the Java main class and the main logic of execution, as follows:

// HotSpot VM里对JNI的CallStaticVoidMethod的实现。留意要传给Java方法的参数
// 以C的可变长度参数传入,这个函数将其收集打包为JNI_ArgumentPusherVaArg对象
-> jni_CallStaticVoidMethod()

     // 这里进一步将要传给Java的参数转换为JavaCallArguments对象传下去    
     -> jni_invoke_static()

        // 真正底层实现的开始。这个方法只是层皮,把JavaCalls::call_helper()
        // 用os::os_exception_wrapper()包装起来,目的是设置HotSpot VM的C++层面的异常处理
        -> JavaCalls::call()   

           -> JavaCalls::call_helper()
              -> //... 检查目标方法是否为空方法,是的话直接返回
              -> //... 检查目标方法是否“首次执行前就必须被编译”,是的话调用JIT编译器去编译目标方法
              -> //... 获取目标方法的解释模式入口from_interpreted_entry,下面将其称为entry_point
              -> //... 确保Java栈溢出检查机制正确启动
              -> //... 创建一个JavaCallWrapper,用于管理JNIHandleBlock的分配与释放,
                 // 以及在调用Java方法前后保存和恢复Java的frame pointer/stack pointer

              //... StubRoutines::call_stub()返回一个指向call stub的函数指针,
              // 紧接着调用这个call stub,传入前面获取的entry_point和要传给Java方法的参数等信息
              -> StubRoutines::call_stub()(...) 
                 // call stub是在VM初始化时生成的。对应的代码在
                 // StubGenerator::generate_call_stub()函数中
                 -> //... 把相关寄存器的状态调整到解释器所需的状态
                 -> //... 把要传给Java方法的参数从JavaCallArguments对象解包展开到解释模
                    // 式calling convention所要求的位置
                 -> //... 跳转到前面传入的entry_point,也就是目标方法的from_interpreted_entry

                    -> //... 在-Xcomp模式下,实际跳入的是i2c adapter stub,将解释模式calling convention
                       // 传入的参数挪到编译模式calling convention所要求的位置
                           -> //... 跳转到目标方法被JIT编译后的代码里,也就是跳到 nmethod 的 VEP 所指向的位置
                                -> //... 正式开始执行目标方法被JIT编译好的代码 <- 这里就是"main()方法的真正入口"

The next three steps are in the mode of compilation and execution, but we will start to study from the interpretation and execution, so we need to configure the -Xint option for the virtual machine. With this option, the main() method of the Java main class will be interpreted and executed. .

In the process of calling the main() method of the Java main class, we saw that the virtual machine indirectly calls the main() method through the JavaCalls::call() function. In the next article, we will study the specific calling logic.

Part 2-Java virtual machine calls the main() method of the Java main class like this

In the previous article Part 1-About the Java virtual machine HotSpot , in the beginning, the functions of call_static(), call_virtual(), etc. are introduced, which will call the JavaCalls::call() function. We look at the call of the main() method in the Java class. The call stack is as follows:

JavaCalls::call_helper() at javaCalls.cpp    
os::os_exception_wrapper() at os_linux.cpp    
JavaCalls::call() at javaCalls.cpp
jni_invoke_static() at jni.cpp    
jni_CallStaticVoidMethod() at jni.cpp    
JavaMain() at java.c
start_thread() at pthread_create.c
clone() at clone.S

This is the call stack on Linux, and the main() method is executed through the JavaCalls::call_helper() function. The starting function of the stack is clone(). This function creates a separate stack space for each process (Linux process corresponds to Java thread). This stack space is shown in the figure below.

On the Linux operating system, the address of the stack extends to the lower address, so the unused stack space is below the used stack space. Each blue cell in the figure represents the stack frame of the corresponding method, and the stack is composed of stack frames one by one. The stack frame of the native method, the Java interpretation stack frame, and the Java compilation stack frame are all allocated in the yellow area, so they are parasitic in the host stack. These different stack frames are closely adjacent to each other, so no space is generated. Fragmentation issues, and this layout is very conducive to stack traversal. The call stack given above is obtained by traversing one by one stack frame, and the traversal process is also the process of stack unwinding. Subsequent handling of exceptions, running jstack printing thread stacks, GC searching for root references, etc. will all unwind the stack, so stack unwinding must be introduced later.

Let's continue to look at the JavaCalls::call_helper() function. There is a very important call in this function, as follows:

// do call
{
    JavaCallWrapper link(method, receiver, result, CHECK);
    {
      HandleMark hm(thread);  // HandleMark used by HandleMarkCleaner
      StubRoutines::call_stub()(
         (address)&link,
         result_val_address,              
         result_type,
         method(),
         entry_point,
         args->parameters(),
         args->size_of_parameters(),
         CHECK
      );

      result = link.result();  
      // Preserve oop return value across possible gc points
      if (oop_result_flag) {
        thread->set_vm_result((oop) result->get_jobject());
      }
    }
}

Call the StubRoutines::call_stub() function to return a function pointer, and then use the function pointer to call the function pointed to by the function pointer. Calling by function pointer is the same as calling by function name. Here we need to be clear that the target function to be called is still a C/C++ function, so when another C/C++ function is called from a C/C++ function, the calling convention must be observed. . This calling convention will specify how to pass parameters to the called function (Callee) and where the return value of the called function will be stored.

Let's briefly talk about the C/C++ function calling convention under the Linux X86 architecture. Under this convention, the following registers are used to pass parameters:

The first parameter: rdi c_rarg0
The second parameter: rsi c_rarg1
The third parameter: rdx c_rarg2
The fourth parameter: rcx c_rarg3
The fifth parameter: r8 c_rarg4
The sixth parameter: r9 c_rarg5

In the function call, 6 and less than 6 are passed by the following registers, and the corresponding registers are used in HotSpot through the more understandable alias c_rarg*. If there are more than six parameters, the program will use the call stack to pass those additional parameters.

Count how many parameters we passed when calling through a function pointer? 8, then the latter two need to be passed through the stack of the calling function (Caller). These two parameters are args->size_of_parameters() and CHECK (this is a macro, and the thread object is passed after expansion).

So our call stack changes to the following state when calling the function pointed to by the function pointer:

On the right is the content of the specific call_helper() stack frame. We push the thread and parameter size into the call stack. In fact, in the process of adjusting the target function, a new stack frame will be opened and the return address and the parameter size will be pushed. The bottom of the call stack, we will introduce it in detail in the next article. Let's first introduce the implementation of the JavaCalls::call_helper() function. We will introduce it in 3 parts.

1. Check whether the target method "must be compiled before the first execution", if yes, call the JIT compiler to compile the target method;

The code is implemented as follows:

void JavaCalls::call_helper(
 JavaValue* result, 
 methodHandle* m, 
 JavaCallArguments* args, 
 TRAPS
) {
  methodHandle method = *m;
  JavaThread* thread = (JavaThread*)THREAD;
  ...

  assert(!thread->is_Compiler_thread(), "cannot compile from the compiler");
  if (CompilationPolicy::must_be_compiled(method)) {
    CompileBroker::compile_method(method, InvocationEntryBci,
                                  CompilationPolicy::policy()->initial_compile_level(),
                                  methodHandle(), 0, "must_be_compiled", CHECK);
  }
  ...
}

For the main() method, if the -Xint option is configured, it is executed in interpreted mode, so the logic of the above compile_method() function will not be followed. Later, when we want to study the compilation and execution, we can force the compilation and execution, and then check the execution process.

2. Get the interpreted mode entry from_interpreted_entry of the target method, which is the value of entry_point. The obtained entry_point is to prepare the stack frame for the Java method call, and point the code call pointer to the memory address of the first bytecode of the method. Entry_point is equivalent to the encapsulation of method, and different method types have different entry_points.

Then look at the code implementation of the call_helper() function, as follows:

address entry_point = method->from_interpreted_entry();

Call the from_interpreted_entry() function of the method to get the value of the _from_interpreted_entry attribute in the Method instance. Where is this value set? We will introduce in detail later.

3. To call the call_stub() function, 8 parameters need to be passed. This code was given before, so I won't give it here again. Below we introduce these parameters in detail, as follows:

(1) link The type of this variable is JavaCallWrapper. This variable is very important for the stack unwinding process, which will be described in detail later;

(2) The result_val_address function returns the value address;

(3) The result_type function return type;

(4) method() The method currently to be executed. Through this parameter, you can get all the metadata information of the Java method, including the most important bytecode information, so that the method can be executed according to the interpretation of the bytecode information;

(5) entry_point HotSpot will inevitably call the CallStub function pointer every time it calls a Java function. The value of this function pointer is taken from _call_stub_entry, and HotSpot points to the called function address through _call_stub_entry. Before calling the function, you must go through entry_point. HotSpot actually gets the first bytecode command corresponding to the Java method from the method() object through entry_point, which is also the entry point for the entire function;

(6) args->parameters() describes the input parameter information of the Java function;

(7) args->size_of_parameters() The memory size in words that need to be occupied by the parameters

(8) CHECK the current thread object.

The most important thing here is entry_point, which is also the content to be introduced in the next article.

Part 3-the creation of a new stack frame for CallStub

In the previous article Part 2-JVM virtual machine calls the main() method of the Java main class like this, we introduced that a function is called in the call_helper() function through a function pointer, as follows:

StubRoutines::call_stub()(
         (address)&link,
         result_val_address,              
         result_type,
         method(),
         entry_point,
         args->parameters(),
         args->size_of_parameters(),
         CHECK
);

Among them, calling the StubRoutines::call_stub() function will return a function pointer. Finding out the implementation of the function pointed to by this function pointer is the focus of this article. The implementation of the called call_stub() function is as follows:

Source code location: openjdk/hotspot/src/share/vm/runtime/stubRoutines.hpp

static CallStub  call_stub() { 
    return CAST_TO_FN_PTR(CallStub, _call_stub_entry); 
}

The call_stub() function returns a function pointer that points to a specific method that depends on the operating system and cpu architecture. The reason is simple. To execute native code, you have to look at what cpu architecture is in order to determine the register, and what os is in order to determine the ABI.

Among them, CAST_TO_FN_PTR is a macro, which is defined as follows:

Source code location: /src/share/vm/runtime/utilities/globalDefinitions.hpp

#define CAST_TO_FN_PTR(func_type, value) ((func_type)(castable_address(value)))

After macro replacement and expansion of the call_stub() function, it will become the following form:

static CallStub call_stub(){
    return (CallStub)( castable_address(_call_stub_entry) );
}

The definition of CallStub is as follows:

Source code location: openjdk/hotspot/src/share/vm/runtime/stubRoutines.hpp

typedef void (*CallStub)(
    // 连接器
    address   link,    
    // 函数返回值地址
    intptr_t* result, 
    //函数返回类型 
    BasicType result_type, 
    // JVM内部所表示的Java方法对象
    Method*   method, 
    // JVM调用Java方法的例程入口。JVM内部的每一段
    // 例程都是在JVM启动过程中预先生成好的一段机器指令。
    // 要调用Java方法, 必须经过本例程, 
    // 即需要先执行这段机器指令,然后才能跳转到Java方法
    // 字节码所对应的机器指令去执行
    address   entry_point, 
    intptr_t* parameters,
    int       size_of_parameters,
    TRAPS
);

A function pointer type is defined as above, and the pointed function declares 8 formal parameters.

The castable_address() function called in the call_stub() function is defined in the globalDefinitions.hpp file, and the specific implementation is as follows:

inline address_word  castable_address(address x)  { 
    return address_word(x) ; 
}

address_word is a certain custom type, defined in the globalDefinitions.hpp file as follows:

typedef   uintptr_t     address_word;

Among them, uintptr_t is also a custom type. The definition in the globalDefinitions_gcc.hpp file is used under the operating system of the Linux kernel. The specific definition is as follows:

typedef  unsigned int  uintptr_t;

In this way, the call_stub() function is actually equivalent to the following implementation form:

static CallStub call_stub(){
    return (CallStub)( unsigned int(_call_stub_entry) );
}

Cast _call_stub_entry to unsigned int type, and then cast it to CallStub type. CallStub is a function pointer, so _call_stub_entry should also be a function pointer, not an ordinary unsigned integer.

In the call_stub() function, the definition of _call_stub_entry is as follows:

address StubRoutines::_call_stub_entry = NULL;

The initialization of _call_stub_entry is in the generate_initial() function under openjdk/hotspot/src/cpu/x86/vm/stubGenerator_x86_64.cpp. The call chain is as follows:

StubGenerator::generate_initial()   stubGenerator_x86_64.cpp    
StubGenerator::StubGenerator()      stubGenerator_x86_64.cpp
StubGenerator_generate()            stubGenerator_x86_64.cpp    
StubRoutines::initialize1()         stubRoutines.cpp    
stubRoutines_init1()                stubRoutines.cpp    
init_globals()                      init.cpp
Threads::create_vm()                thread.cpp
JNI_CreateJavaVM()                  jni.cpp
InitializeJVM()                     java.c
JavaMain()                          java.c

The StubGenerator class is defined in the stubGenerator_x86_64.cpp file in the openjdk/hotspot/src/cpu/x86/vm directory. The generate_initial() method in this file will initialize the call_stub_entry variable, as follows:

StubRoutines::_call_stub_entry = generate_call_stub(StubRoutines::_call_stub_return_address);

Now we finally find the implementation logic of the function pointed to by the function pointer. This logic is implemented by calling the generate_call_stub() function.

However, after checking, we found that this function pointer is not a C++ function, but a machine instruction fragment. We can regard it as an instruction fragment generated by a C++ function compiled by a C++ compiler. There are the following call statements in the generate_call_stub() function:

__ enter();
__ subptr(rsp, -rsp_after_call_off * wordSize);

These two pieces of code directly generate machine instructions, but in order to view the machine instructions, we use the HSDB tool to decompile them into more readable assembly instructions. as follows:

push   %rbp         
mov    %rsp,%rbp
sub    $0x60,%rsp

These 3 assemblers are very typical instructions for opening up a new stack frame. Before, we introduced the stack state before calling through the function pointer, as follows:

Then after running the above 3 assembly, the stack state becomes the following state:

What we need to pay attention to is the orientation of old %rbp and old %rsp when it is not running to open a new stack frame (CallStub() stack frame), as well as the new %rbp and new% when opening a new stack frame (CallStub() stack frame) The point of rsp. Also note that saved rbp saves old %rbp, this value is very important for stack expansion, because it can be traversed continuously, and finally all stack frames can be found.

Next, let's look at the implementation of the generate_call_stub() function, as follows:

address generate_call_stub(address& return_address) {
    ...
    address start = __ pc();
 

    const Address rsp_after_call(rbp, rsp_after_call_off * wordSize);
 
    const Address call_wrapper  (rbp, call_wrapper_off   * wordSize);
    const Address result        (rbp, result_off         * wordSize);
    const Address result_type   (rbp, result_type_off    * wordSize);
    const Address method        (rbp, method_off         * wordSize);
    const Address entry_point   (rbp, entry_point_off    * wordSize);
    const Address parameters    (rbp, parameters_off     * wordSize);
    const Address parameter_size(rbp, parameter_size_off * wordSize);
 
    const Address thread        (rbp, thread_off         * wordSize);
 
    const Address r15_save(rbp, r15_off * wordSize);
    const Address r14_save(rbp, r14_off * wordSize);
    const Address r13_save(rbp, r13_off * wordSize);
    const Address r12_save(rbp, r12_off * wordSize);
    const Address rbx_save(rbp, rbx_off * wordSize);
 
    // 开辟新的栈帧
    __ enter();
    __ subptr(rsp, -rsp_after_call_off * wordSize);
 
    // save register parameters
    __ movptr(parameters,   c_rarg5); // parameters
    __ movptr(entry_point,  c_rarg4); // entry_point
 
 
    __ movptr(method,       c_rarg3); // method
    __ movl(result_type,  c_rarg2);   // result type
    __ movptr(result,       c_rarg1); // result
    __ movptr(call_wrapper, c_rarg0); // call wrapper
 
    // save regs belonging to calling function
    __ movptr(rbx_save, rbx);
    __ movptr(r12_save, r12);
    __ movptr(r13_save, r13);
    __ movptr(r14_save, r14);
    __ movptr(r15_save, r15);
 
    const Address mxcsr_save(rbp, mxcsr_off * wordSize);
    {
      Label skip_ldmx;
      __ stmxcsr(mxcsr_save);
      __ movl(rax, mxcsr_save);
      __ andl(rax, MXCSR_MASK);    // Only check control and mask bits
      ExternalAddress mxcsr_std(StubRoutines::addr_mxcsr_std());
      __ cmp32(rax, mxcsr_std);
      __ jcc(Assembler::equal, skip_ldmx);
      __ ldmxcsr(mxcsr_std);
      __ bind(skip_ldmx);
    }

    // ... 省略了接下来的操作
}

We have already introduced the logic of opening up a new stack frame. The following is to store the 6 parameters passed by call_helper() in the register into the CallStub() stack frame. In addition to storing these parameters, other registers also need to be stored. Because the next operation of the function is to prepare parameters for the Java method and call the Java method, we don’t know whether the Java method will destroy the value in these registers, so we need to save it and restore it after the call is complete. .

The generated assembly code is as follows:

mov      %r9,-0x8(%rbp)
mov      %r8,-0x10(%rbp)
mov      %rcx,-0x18(%rbp)
mov      %edx,-0x20(%rbp)
mov      %rsi,-0x28(%rbp)
mov      %rdi,-0x30(%rbp)
mov      %rbx,-0x38(%rbp)
mov      %r12,-0x40(%rbp)
mov      %r13,-0x48(%rbp)
mov      %r14,-0x50(%rbp)
mov      %r15,-0x58(%rbp)
// stmxcsr是将MXCSR寄存器中的值保存到-0x60(%rbp)中
stmxcsr  -0x60(%rbp)  
mov      -0x60(%rbp),%eax
and      $0xffc0,%eax // MXCSR_MASK = 0xFFC0
// cmp通过第2个操作数减去第1个操作数的差,根据结果来设置eflags中的标志位。
// 本质上和sub指令相同,但是不会改变操作数的值
cmp      0x1762cb5f(%rip),%eax  # 0x00007fdf5c62d2c4 
// 当ZF=1时跳转到目标地址
je       0x00007fdf45000772 
// 将m32加载到MXCSR寄存器中
ldmxcsr  0x1762cb52(%rip)      # 0x00007fdf5c62d2c4  

After loading these parameters, it is as shown in the figure below.

In the next article, we will continue to introduce the rest of the implementation of the generate_call_stub() function.

Part 4-JVM finally starts to call the main() method of the Java main class

In the third part of the previous article-the creation of the CallStub new stack frame, we introduced part of the implementation of the generate_call_stub() function, and completed the operation of pushing parameters into the CallStub stack frame. The state at this time is shown in the figure below.

Continue to see the implementation of the generate_call_stub() function. Next, the thread register will be loaded. The code is as follows:

__ movptr(r15_thread, thread);
__ reinit_heapbase();

The generated assembly code is as follows:

mov    0x18(%rbp),%r15  
mov    0x1764212b(%rip),%r12   # 0x00007fdf5c6428a8

In contrast to the above stack frame, you can see that 0x18 (%rbp) is the location where thread is stored, and this parameter is stored in the %r15 register.

If there are parameters when calling the function, you need to pass parameters, the code is as follows:

Label parameters_done;
// parameter_size拷贝到c_rarg3即rcx寄存器中
__ movl(c_rarg3, parameter_size);
// 校验c_rarg3的数值是否合法。两操作数作与运算,仅修改标志位,不回送结果
__ testl(c_rarg3, c_rarg3);
// 如果不合法则跳转到parameters_done分支上
__ jcc(Assembler::zero, parameters_done);

// 如果执行下面的逻辑,那么就表示parameter_size的值不为0,也就是需要为
// 调用的java方法提供参数
Label loop;
// 将地址parameters包含的数据即参数对象的指针拷贝到c_rarg2寄存器中
__ movptr(c_rarg2, parameters);       
// 将c_rarg3中值拷贝到c_rarg1中,即将参数个数复制到c_rarg1中
__ movl(c_rarg1, c_rarg3);            
__ BIND(loop);
// 将c_rarg2指向的内存中包含的地址复制到rax中
__ movptr(rax, Address(c_rarg2, 0));
// c_rarg2中的参数对象的指针加上指针宽度8字节,即指向下一个参数
__ addptr(c_rarg2, wordSize);       
// 将c_rarg1中的值减一
__ decrementl(c_rarg1);            
// 传递方法调用参数
__ push(rax);                       
// 如果参数个数大于0则跳转到loop继续
__ jcc(Assembler::notZero, loop);

Here is a loop for passing parameters, which is equivalent to the following code:

while(%esi){
   rax = *arg
   push_arg(rax)
   arg++;   // ptr++
   %esi--;  // counter--
}

The generated assembly code is as follows:

// 将栈中parameter size送到%ecx中
mov    0x10(%rbp),%ecx   
// 做与运算,只有当%ecx中的值为0时才等于0 
test   %ecx,%ecx          
// 没有参数需要传递,直接跳转到parameters_done即可
je     0x00007fdf4500079a 
// -- loop --
// 汇编执行到这里,说明paramter size不为0,需要传递参数
mov    -0x8(%rbp),%rdx
mov    %ecx,%esi
mov    (%rdx),%rax
add    $0x8,%rdx
dec    %esi
push   %rax
// 跳转到loop
jne    0x00007fdf4500078e  

Because the Java method needs to be called, the actual parameters are pressed for the Java method, that is, the parameter size is pressed into the parameters taken from the parameters. The stack after pressing the parameters is shown in the figure below.

When the parameters that need to call the Java method are ready, the Java method will be called next. Here I need to highlight the method calling convention when Java interprets the execution. Unlike the calling convention of C/C++ under x86, which does not need to pass parameters through registers, but passes parameters through the stack, it is more straightforward. , The parameters are passed through the local variable table, so the argument word1 ... argument word n ​​in the CallStub() function stack frame in the above figure is actually part of the local variable table of the called Java method.

Next, look at the code that calls the Java method, as follows:

// 调用Java方法
// -- parameters_done --

__ BIND(parameters_done); 
// 将method地址包含的数据接Method*拷贝到rbx中
__ movptr(rbx, method);            
// 将解释器的入口地址拷贝到c_rarg1寄存器中
__ movptr(c_rarg1, entry_point);    
// 将rsp寄存器的数据拷贝到r13寄存器中
__ mov(r13, rsp);                   

// 调用解释器的解释函数,从而调用Java方法
// 调用的时候传递c_rarg1,也就是解释器的入口地址
__ call(c_rarg1); 

The generated assembly code is as follows:

// 将Method*送到%rbx中
mov     -0x18(%rbp),%rbx  
// 将entry_point送到%rsi中
mov     -0x10(%rbp),%rsi  
// 将调用者的栈顶指针保存到%r13中
mov     %rsp,%r13    
// 调用Java方法     
callq   *%rsi             

Note that after calling the callq instruction, the address of the next instruction of the callq instruction will be pushed onto the stack, and then jump to the address specified by the first operand, which is the address represented by *%rsi. Pushing the address of the next instruction is to allow the function to return from the sub-function by jumping to the address on the stack.

The callq instruction calls entry_point. entry_point will be introduced in detail later.

Part 5-Pop up the stack frame after calling the Java method and process the returned result

In the previous article Chapter 4-JVM finally started to call the main() method of the Java main class. introduced the call of entry point through callq, but we have not finished the implementation of the generate_call_stub() function. Next, in the generate_call_stub() function, the return value after calling the Java method will be processed. At the same time, the stack operation needs to be performed, which is to restore the stack to the state before calling the Java method. What is the state before the call? In the second part-JVM virtual machine calls the main () method of the Java main class like this, this state is shown in the following figure.

The following code of the generate_call_stub() function is implemented as follows:

// 保存方法调用结果依赖于结果类型,只要不是T_OBJECT, T_LONG, T_FLOAT or T_DOUBLE,都当做T_INT处理
// 将result地址的值拷贝到c_rarg0中,也就是将方法调用的结果保存在rdi寄存器中,注意result为函数返回值的地址
__ movptr(c_rarg0, result);     

Label is_long, is_float, is_double, exit;

// 将result_type地址的值拷贝到c_rarg1中,也就是将方法调用的结果返回的类型保存在esi寄存器中
__ movl(c_rarg1, result_type);  

// 根据结果类型的不同跳转到不同的处理分支
__ cmpl(c_rarg1, T_OBJECT);
__ jcc(Assembler::equal, is_long);
__ cmpl(c_rarg1, T_LONG);
__ jcc(Assembler::equal, is_long);
__ cmpl(c_rarg1, T_FLOAT);
__ jcc(Assembler::equal, is_float);
__ cmpl(c_rarg1, T_DOUBLE);
__ jcc(Assembler::equal, is_double);

// 当逻辑执行到这里时,处理的就是T_INT类型,
// 将rax中的值写入c_rarg0保存的地址指向的内存中
// 调用函数后如果返回值是int类型,则根据调用约定
// 会存储在eax中
__ movl(Address(c_rarg0, 0), rax); 

__ BIND(exit);


// 将rsp_after_call中保存的有效地址拷贝到rsp中,即将rsp往高地址方向移动了,
// 原来的方法调用实参argument 1、...、argument n,
// 相当于从栈中弹出,所以下面语句执行的是退栈操作
__ lea(rsp, rsp_after_call);  // lea指令将地址加载到寄存器中

Here we need to pay attention to result and result_type. The result will be passed when the call_helper() function is called, that is, it will instruct the call_helper() function to store the return value after calling the Java method. For the result whose type is JavaValue, the return type is actually set before the call, so the result_type variable above only needs to get the result type from JavaValue. For example, when calling the main() method of the Java main class, the return type will be set to T_VOID in the jni_CallStaticVoidMethod() function and the jni_invoke_static() function, that is, the main() method returns void.

The generated assembly code is as follows:

// 栈中的-0x28位置保存result
mov    -0x28(%rbp),%rdi  
// 栈中的-0x20位置保存result type
mov    -0x20(%rbp),%esi  
cmp    $0xc,%esi         // 是否为T_OBJECT类型
je     0x00007fdf450007f6
cmp    $0xb,%esi         // 是否为T_LONG类型
je     0x00007fdf450007f6
cmp    $0x6,%esi         // 是否为T_FLOAT类型
je     0x00007fdf450007fb
cmp    $0x7,%esi         // 是否为T_DOUBLE类型
je     0x00007fdf45000801

// 如果是T_INT类型,直接将返回结果%eax写到栈中-0x28(%rbp)的位置
mov    %eax,(%rdi)       

// -- exit --

// 将rsp_after_call的有效地址拷到rsp中
lea    -0x60(%rbp),%rsp  

In order for everyone to see clearly, I will post the state of the stack frame before calling the Java method, as follows:

It can be seen from the figure that the location pointed to by the -0x60 (%rbp) address does not include the actual argument word 1 ... argument word n ​​that was pressed when calling the Java method. So now rbp and rsp are the positions pointed to in the figure.

Next, restore the previously saved caller-save register, which is also part of the calling convention, as follows:

__ movptr(r15, r15_save);
__ movptr(r14, r14_save);
__ movptr(r13, r13_save);
__ movptr(r12, r12_save);
__ movptr(rbx, rbx_save);

__ ldmxcsr(mxcsr_save); 

The generated assembly code is as follows:

mov      -0x58(%rbp),%r15
mov      -0x50(%rbp),%r14
mov      -0x48(%rbp),%r13
mov      -0x40(%rbp),%r12
mov      -0x38(%rbp),%rbx
ldmxcsr  -0x60(%rbp)

After popping up the actual parameters saved for invoking the Java method and restoring the caller-save register, continue to perform the unstack operation, the implementation is as follows:

// restore rsp
__ addptr(rsp, -rsp_after_call_off * wordSize);

// return
__ pop(rbp);
__ ret(0);

The generated assembly code is as follows:

// %rsp加上0x60,也就是执行退栈操作,也就相
// 当于弹出了callee_save寄存器和压栈的那6个参数
add    $0x60,%rsp 
pop    %rbp
// 方法返回,指令中的q表示64位操作数,就是指
// 的栈中存储的return address是64位的
retq  

Remember that in the previous chapter 3-CallStub new stack frame creation, the creation of the new stack frame was completed through the following assembly:

push   %rbp         
mov    %rsp,%rbp 
sub    $0x60,%rsp

Now when exiting this stack frame, add $0x60 to the address pointed to by %rsp, and restore the point of %rbp at the same time. Then it jumps to the instruction pointed to by the return address to continue execution.

In order to make it easier for everyone to view, I have once again given the picture used before, this picture is the picture before the stack is unstacked:

After exiting the stack, as shown in the figure below.

As for the paramter size and thread, the JavaCalls::call_hlper() function is responsible for releasing, which is part of the C/C++ calling convention. So if we don't look at these two parameters, we have completely returned to the stack shown in the first picture given in this article.

You should not be unfamiliar with the above pictures. We have given them step by step when creating a stack frame, and you will exit how you create it now.

As mentioned before, when the Java method returns the int type (if it returns char, boolean, short, etc., it is converted to the int type), according to the Java method calling convention, the returned int value will be stored in %rax; if it returns an object , Then what is stored in %rax is the address of this object, then how do you distinguish between the address and the int value? The answer is that it can be distinguished by the return type; if it returns a value of non-int, non-object type? We continue to look at the implementation logic of the generate_call_stub() function:

// handle return types different from T_INT
__ BIND(is_long);
__ movq(Address(c_rarg0, 0), rax);
__ jmp(exit);

__ BIND(is_float);
__ movflt(Address(c_rarg0, 0), xmm0);
__ jmp(exit);

__ BIND(is_double);
__ movdbl(Address(c_rarg0, 0), xmm0);
__ jmp(exit); 

The corresponding assembly code is as follows:

// -- is_long --
mov    %rax,(%rdi)
jmp    0x00007fdf450007d4

// -- is_float --
vmovss %xmm0,(%rdi)
jmp    0x00007fdf450007d4

// -- is_double --
vmovsd %xmm0,(%rdi)
jmp    0x00007fdf450007d4

When the long type is returned, it is also stored in %rax. Because Java's long type is 64-bit, the code we analyzed is also a 64-bit implementation under x86, so the %rax register is also 64-bit and can hold 64-bit numbers; when the return is In the case of float or double, it is stored in %xmm0.

Combining this article and the previous articles, we should learn the calling conventions of C/C++ and the calling conventions of Java methods in interpretation and execution (including how to pass parameters, how to receive return values, etc.). If you don’t understand, read more. A few times the article will have a clear understanding.

Part 6-Creation of New Stack Frame for Java Method

In of two -JVM virtual machine to invoke this main Java class main () method mentioned as a code Introducing JavaCalls :: call_helper () function to achieve:

address entry_point = method->from_interpreted_entry();

This parameter will be passed as an actual parameter to the "function" pointed to by the StubRoutines::call_stub() function pointer, and then in Chapter 4-JVM finally starts to call the main() method of the Java main class. It is introduced to call entry_point through the callq instruction. So what exactly is this entry_point? We will introduce in detail in this article.

First look at the implementation of the from_interpreted_entry() function, as follows:

Source code location: /openjdk/hotspot/src/share/vm/oops/method.hpp

volatile address from_interpreted_entry() const{ 
      return (address)OrderAccess::load_ptr_acquire(&_from_interpreted_entry); 
}

_from_interpreted_entry is just an attribute defined in the Method class. The above method directly returns the value of this attribute. So when is this attribute assigned? In fact, it is set when the method is connected (that is, the method connection will be performed in the class connection stage of the class life cycle). When the method is connected, the following method will be called:

void Method::link_method(methodHandle h_method, TRAPS) {
  // ...
  address entry = Interpreter::entry_for_method(h_method);
  // Sets both _i2i_entry and _from_interpreted_entry
  set_interpreter_entry(entry);
  // ...
}

First, call the Interpreter::entry_for_method() function to get the method entry according to the specific method type. After getting the entry entry, it will call the set_interpreter_entry() function to save the value to the corresponding attribute. The implementation of the set_interpreter_entry() function is very simple, as follows:

void set_interpreter_entry(address entry) { 
    _i2i_entry = entry;  
    _from_interpreted_entry = entry; 
}

You can see that the entry value is set for the _from_interpreted_entry attribute.

Let's take a look at the implementation of the entry_for_method() function, as follows:

static address entry_for_method(methodHandle m)  { 
     return entry_for_kind(method_kind(m)); 
}

First get the type corresponding to the method through the method_kind() function, and then call the entry_for_kind() function to get the entry_point corresponding to the method according to the method type. The implementation of the called entry_for_kind() function is as follows:

static address entry_for_kind(MethodKind k){ 
      return _entry_table[k]; 
}

Here directly returns the entry_point address of the corresponding method type in the _entry_table array.

This involves the type MethodKind of the Java method. Since entry_point enters the Java world and executes the logic related to the Java method, entry_point must create a new stack frame for the corresponding Java method, but the stack frames of different methods are actually different. Yes, such as Java ordinary methods, Java synchronous methods, Java methods with native keywords, etc., so all methods are classified, and different types get different entry_point entries. Exactly what types are there, we can look at the enumeration constants defined in the MethodKind enumeration class:

enum MethodKind {
    zerolocals,  // 普通的方法             
    zerolocals_synchronized,  // 普通的同步方法         
    native,  // native方法
    native_synchronized,  // native同步方法
    ...
}

Of course there are other types, but the most important ones are the four types of methods defined in the above enumeration class.

In order to find the entry_point entry corresponding to a certain Java method as soon as possible, the corresponding relationship is stored in _entry_table, so the entry_for_kind() function can quickly obtain the entry_point entry corresponding to the method. There is a special method for assigning values ​​to elements in an array:

void AbstractInterpreter::set_entry_for_kind(AbstractInterpreter::MethodKind kind, address entry) {
  _entry_table[kind] = entry;
}

So when will the set_entry_for_kind() function be called? The answer lies in the TemplateInterpreterGenerator::generate_all() function. The generate_all() function will call the generate_method_entry() function to generate the entry_point of each Java method. Each time an entry_point corresponding to the method type is generated Save to _entry_table.

The following is a detailed introduction to the implementation logic of the generate_all() function, which will be called when HotSpot is started to generate entry_points for various Java methods. The call stack is as follows:

TemplateInterpreterGenerator::generate_all()  templateInterpreter.cpp
InterpreterGenerator::InterpreterGenerator()  templateInterpreter_x86_64.cpp
TemplateInterpreter::initialize()    templateInterpreter.cpp
interpreter_init()                   interpreter.cpp
init_globals()                       init.cpp
Threads::create_vm()                 thread.cpp
JNI_CreateJavaVM()                   jni.cpp
InitializeJVM()                      java.c
JavaMain()                           java.c
start_thread()                       pthread_create.c

The called generate_all() function will generate a series of entries of some common codes and InterpreterCodelet of all bytecodes executed during HotSpot running. Some very important entry implementation logic will be introduced in detail later, here we will only look at the ordinary and no The Java method modified by the native keyword generates the logic of the entry. The following is implemented in the generate_all() function:

#define method_entry(kind)                                                                    \
{                                                                                             \
    CodeletMark cm(_masm, "method entry point (kind = " #kind ")");                           \
    Interpreter::_entry_table[Interpreter::kind] = generate_method_entry(Interpreter::kind);  \
}  

method_entry(zerolocals)

The method_entry is a macro. After expansion, the method_entry(zerolocals) statement above becomes the following form:

Interpreter::_entry_table[Interpreter::zerolocals] = generate_method_entry(Interpreter::zerolocals);

The _entry_table variable is defined in the AbstractInterpreter class, as follows:

static address  _entry_table[number_of_method_entries];  

number_of_method_entries represents the total number of method types, and the corresponding method entry can be obtained by using the method type as an array subscript. Call the generate_method_entry() function to generate corresponding method entries for various types of methods. The implementation of the generate_method_entry() function is as follows:

address AbstractInterpreterGenerator::generate_method_entry(AbstractInterpreter::MethodKind kind) {
  bool                   synchronized = false;
  address                entry_point = NULL;
  InterpreterGenerator*  ig_this = (InterpreterGenerator*)this;

  // 根据方法类型kind生成不同的入口
  switch (kind) { 
  // 表示普通方法类型
  case Interpreter::zerolocals :
      break;
  // 表示普通的、同步方法类型
  case Interpreter::zerolocals_synchronized: 
      synchronized = true;
      break;
  // ...
  }

  if (entry_point) {
     return entry_point;
  }

  return ig_this->generate_normal_entry(synchronized);
}

Zerolocals means normal Java method calls, including the main() method of the Java program. For zerolocals, the ig_this->generate_normal_entry() function is called to generate the entry. The generate_normal_entry() function generates a stack for the executed method, and the stack is composed of three parts: the local variable table (used to store the incoming parameters and the local variables of the called method), the Java method stack frame data, and the operand stack. So the entry_point routine (actually a fragment of machine instructions, called stub in English) will create these 3 parts to assist the execution of the Java method.

Let's go back to the knowledge points introduced at the beginning and call the entry_point routine through the callq instruction. The state of the stack frame at this time was introduced in Part 4-JVM finally started to call the main() method of the Java main class. For the convenience of everyone's reading, here is again:

Note that when the callq instruction is executed, the return address of the function will be stored on the top of the stack, so the return address item will be pushed in the above figure.

When the CallStub() function calls the entry_point generated by the generate_normal_entry() function through the callq instruction, there are several registers that store important values, as follows:

rbx -> Method*
r13 -> sender sp
rsi -> entry point

The following is to analyze the implementation logic of the generate_normal_entry() function, which is the most important part of calling the Java method. The important implementation logic of the function is as follows:

address InterpreterGenerator::generate_normal_entry(bool synchronized) {
  // ...
  // entry_point函数的代码入口地址
  address entry_point = __ pc();   
 
  // 当前rbx中存储的是指向Method的指针,通过Method*找到ConstMethod*
  const Address constMethod(rbx, Method::const_offset()); 
  // 通过Method*找到AccessFlags
  const Address access_flags(rbx, Method::access_flags_offset()); 
  // 通过ConstMethod*得到parameter的大小
  const Address size_of_parameters(rdx,ConstMethod::size_of_parameters_offset());
  // 通过ConstMethod*得到local变量的大小
  const Address size_of_locals(rdx, ConstMethod::size_of_locals_offset());
 
  // 上面已经说明了获取各种方法元数据的计算方式,
  // 但并没有执行计算,下面会生成对应的汇编来执行计算
  // 计算ConstMethod*,保存在rdx里面
  __ movptr(rdx, constMethod);                    
  // 计算parameter大小,保存在rcx里面 
  __ load_unsigned_short(rcx, size_of_parameters);

  // rbx:保存基址;rcx:保存循环变量;rdx:保存目标地址;rax:保存返回地址(下面用到)
  // 此时的各个寄存器中的值如下:
  //   rbx: Method*
  //   rcx: size of parameters
  //   r13: sender_sp (could differ from sp+wordSize 
  //        if we were called via c2i ) 即调用者的栈顶地址
  // 计算local变量的大小,保存到rdx
  __ load_unsigned_short(rdx, size_of_locals);
  // 由于局部变量表用来存储传入的参数和被调用方法的局部变量,
  // 所以rdx减去rcx后就是被调用方法的局部变量可使用的大小 
  __ subl(rdx, rcx); 
 
  // ...
 
  // 返回地址是在CallStub中保存的,如果不弹出堆栈到rax,中间
  // 会有个return address使的局部变量表不是连续的,
  // 这会导致其中的局部变量计算方式不一致,所以暂时将返
  // 回地址存储到rax中
  __ pop(rax);
 
  // 计算第1个参数的地址:当前栈顶地址 + 变量大小 * 8 - 一个字大小
  // 注意,因为地址保存在低地址上,而堆栈是向低地址扩展的,所以只
  // 需加n-1个变量大小就可以得到第1个参数的地址
  __ lea(r14, Address(rsp, rcx, Address::times_8, -wordSize));
 
  // 把函数的局部变量设置为0,也就是做初始化,防止之前遗留下的值影响
  // rdx:被调用方法的局部变量可使用的大小
  {
    Label exit, loop;
    __ testl(rdx, rdx);
    // 如果rdx<=0,不做任何操作
    __ jcc(Assembler::lessEqual, exit); 
    __ bind(loop);
    // 初始化局部变量
    __ push((int) NULL_WORD); 
    __ decrementl(rdx); 
    __ jcc(Assembler::greater, loop);
    __ bind(exit);
  }
 
  // 生成固定桢
  generate_fixed_frame(false);
 
  // ... 省略统计及栈溢出等逻辑,后面会详细介绍

  // 如果是同步方法时,还需要执行lock_method()函数,所以
  // 会影响到栈帧布局 
  if (synchronized) {
    // Allocate monitor and lock method
    lock_method();
  } 

  // 跳转到目标Java方法的第一条字节码指令,并执行其对应的机器指令
   __ dispatch_next(vtos);
 
  // ... 省略统计相关逻辑,后面会详细介绍
 
  return entry_point;
}

The implementation of this function seems to be more, but in fact the logical implementation is relatively simple, it is to create the corresponding local variable table according to the actual situation of the called method, and then there are two very important functions, generate_fixed_frame() and dispatch_next(). We will introduce these two functions in detail later.

Before calling the generate_fixed_frame() function, the state of the stack changed to the state shown in the figure below.

Compared with the previous figure, you can see that there are more slots such as local variable 1 ... local variable n. These slots and argument word 1 ... argument word n ​​together form the local variable table of the called Java method. That is the purple part in the picture. In fact, local variable 1 ... local variable n and other slots are part of the stack frame of the called Java method, while argument word 1 ... argument word n ​​is part of the CallStub() function stack frame. These two parts together constitute the local Variable table, the technical term is called stack frame overlap.

In addition, it can be seen that %r14 points to the first parameter of the local variable table, and the return address of the CallStub() function is stored in %rax, and Method* is still stored in %rbx. The values ​​stored in these registers will be used when calling the generate_fixed_frame() function, so we need to emphasize it here.

Part 7-Create a stack frame for a Java method

In Chapter 6-The creation of a new stack frame of the Java method introduced the creation of the local variable table. The state of the stack frame after the creation is shown in the figure below.

The status of each register is shown below.

// %rax寄存器中存储的是返回地址
rax: return address     
// 要执行的Java方法的指针
rbx: Method*          
// 本地变量表指针  
r14: pointer to locals 
// 调用者的栈顶
r13: sender sp 

Pay attention to the return address saved in rax, because the entry_point generated by the generate_normal_entry() function is called through the __call(c_rarg1) statement in the generate_call_stub() function, so when the entry_point is executed, it will return to the generate_call_stub() function Continue to execute the code below the __ call(c_rarg1) statement, which is

Chapter 5-Pop up the stack frame after calling the Java method and process the involved in the return result 1618ccd916539b.

The implementation of the called generate_fixed_frame() function is as follows:

Source code location: openjdk/hotspot/src/cpu/x86/vm/templateInterpreter_x86_64.cpp

void TemplateInterpreterGenerator::generate_fixed_frame(bool native_call) {
  // 把返回地址紧接着局部变量区保存
  __ push(rax);     
  // 为Java方法创建栈帧       
  __ enter();      
  // 保存调用者的栈顶地址        
  __ push(r13);           
   // 暂时将last_sp属性的值设置为NULL_WORD 
  __ push((int)NULL_WORD); 
  // 获取ConstMethod*并保存到r13中
  __ movptr(r13, Address(rbx, Method::const_offset()));     
  // 保存Java方法字节码的地址到r13中
  __ lea(r13, Address(r13, ConstMethod::codes_offset()));    
  // 保存Method*到堆栈上
  __ push(rbx);             
 
  // ProfileInterpreter属性的默认值为true,
  // 表示需要对解释执行的方法进行相关信息的统计
  if (ProfileInterpreter) {
    Label method_data_continue;
    // MethodData结构基础是ProfileData,
    // 记录函数运行状态下的数据
    // MethodData里面分为3个部分,
    // 一个是函数类型等运行相关统计数据,
    // 一个是参数类型运行相关统计数据,
    // 还有一个是extra扩展区保存着
    // deoptimization的相关信息
    // 获取Method中的_method_data属性的值并保存到rdx中
    __ movptr(rdx, Address(rbx,
           in_bytes(Method::method_data_offset())));
    __ testptr(rdx, rdx);
    __ jcc(Assembler::zero, method_data_continue);
    // 执行到这里,说明_method_data已经进行了初始化,
    // 通过MethodData来获取_data属性的值并存储到rdx中
    __ addptr(rdx, in_bytes(MethodData::data_offset()));
    __ bind(method_data_continue);
    __ push(rdx);      
  } else {
    __ push(0);
  }
  
  // 获取ConstMethod*存储到rdx
  __ movptr(rdx, Address(rbx, 
        Method::const_offset()));          
  // 获取ConstantPool*存储到rdx
  __ movptr(rdx, Address(rdx, 
         ConstMethod::constants_offset())); 
 // 获取ConstantPoolCache*并存储到rdx
  __ movptr(rdx, Address(rdx, 
         ConstantPool::cache_offset_in_bytes())); 
  // 保存ConstantPoolCache*到堆栈上
  __ push(rdx); 
  // 保存第1个参数的地址到堆栈上
  __ push(r14); 
 
  if (native_call) {
   // native方法调用时,不需要保存Java
   // 方法的字节码地址,因为没有字节码
    __ push(0); 
  } else {
   // 保存Java方法字节码地址到堆栈上,
   // 注意上面对r13寄存器的值进行了更改
    __ push(r13);
  }
  
  // 预先保留一个slot,后面有大用处
  __ push(0); 
  // 将栈底地址保存到这个slot上
  __ movptr(Address(rsp, 0), rsp); 
}

For ordinary Java methods, the generated assembly code is as follows:

push   %rax
push   %rbp
mov    %rsp,%rbp
push   %r13
pushq  $0x0
mov    0x10(%rbx),%r13
lea    0x30(%r13),%r13 // lea指令获取内存地址本身
push   %rbx
mov    0x18(%rbx),%rdx
test   %rdx,%rdx
je     0x00

HeapDump性能社区
442 声望693 粉丝

有性能问题,上HeapDump性能社区