[JVM source code analysis] the virtual machine interprets and executes Java methods

Keywords: Java jvm

This article is compiled and published by jiumo (Ma Zhi), chief lecturer of HeapDump performance community

Part 29 - calling the main() method of the Java main class

Many articles have been written to introduce the assembly code execution logic corresponding to bytecode instructions, and some assembly code logic corresponding to bytecode instructions have not been introduced. These instructions include method call instructions, synchronization instructions and exception throw instructions. The assembly code implementation logic of these instructions is relatively complex, so we will introduce method call The knowledge points of synchronization and exception handling will be introduced in detail through a large article!

In Chapter 1, the calling process of the main class method main() in Java has been briefly introduced. This article introduces it in detail. The general calling process is shown in the figure below.

The light red function is executed by the main thread, while the other light green part is executed by another thread. The light green thread will eventually be responsible for executing the main() method in the Java main class. In the JavaMain () function, call the LoadMainClass() function to load the Java main class. Next, the JavaMain() function is called as follows:

Source code location: openjdk/jdk/src/share/bin/java.c
 
mainID = (*env)->GetStaticMethodID(
  env, 
  mainClass, 
  "main", 
  "([Ljava/lang/String;)V");

env is of type JNIEnv *. Call the GetStaticMethodID() function defined in JNIEnv type to obtain the method unique ID of the main() method in the Java main class. Calling the GetStaticMethodID() function is to call jni_GetStaticMethodID() function. The implementation of this function is as follows:

Source code location: openjdk/hotspot/src/share/vm/prims/jni.cpp
 
JNI_ENTRY(jmethodID, jni_GetStaticMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig))
  jmethodID ret = get_method_id(env, clazz, name, sig, true, thread);
  return ret;
JNI_END
 
 
static jmethodID get_method_id(
   JNIEnv *env,
   jclass clazz,
   const char *name_str,
   const char *sig,
   bool is_static,
   TRAPS
){
  const char *name_to_probe = (name_str == NULL)
                        ? vmSymbols::object_initializer_name()->as_C_string()
                        : name_str;
  TempNewSymbol name = SymbolTable::probe(name_to_probe, (int)strlen(name_to_probe));
  TempNewSymbol signature = SymbolTable::probe(sig, (int)strlen(sig));
 
  KlassHandle klass(THREAD,java_lang_Class::as_Klass(JNIHandles::resolve_non_null(clazz)));
 
  // Ensure that the java.lang.Class class has been initialized
  klass()->initialize(CHECK_NULL);
 
  Method* m;
  if ( name == vmSymbols::object_initializer_name() || Looking for<init>method
       name == vmSymbols::class_initializer_name() ) { Looking for<clinit>method
    // Because you are looking for a constructor, which has no inheritance characteristics, you will not continue to look in the parent class if the current class cannot be found
    if (klass->oop_is_instance()) {
       // find_ The method() function does not look up
       m = InstanceKlass::cast(klass())->find_method(name, signature); 
    } else {
       m = NULL;
    }
  } else {
    // lookup_ The method () function looks up
    m = klass->lookup_method(name, signature); 
    if (m == NULL && klass->oop_is_instance()) {
       m = InstanceKlass::cast(klass())->lookup_method_in_ordered_interfaces(name, signature);
    }
  }
  return m->jmethod_id();
}

Get jmethod of main() method in Java class_ id.

Source code location: method.hpp
// Get this method's jmethodID -- allocate if it doesn't exist
jmethodID jmethod_id()  {
      methodHandle this_h(this);
      return InstanceKlass::get_jmethod_id(method_holder(), this_h);
}

Instanceklass:: get called\_ jmethod\_ The ID () function obtains the unique ID. the process of how to obtain or generate the ID will not be described in detail here. If you are interested, you can study it yourself.

The following calls are made in the JavaMain() function:

mainArgs = CreateApplicationArgs(env, argv, argc);
 
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);

Call the main() method in the Java main class by calling the callstaticvoidmethod () function. Control is transferred to the main() method in the Java main class. Calling the CallStaticVoidMethod() function calls jni_CallStaticVoidMethod() function. The implementation of this function is as follows:

Source code location: openjdk/hotspot/src/share/vm/prims/jni.cpp 
 
JNI_ENTRY(void, jni_CallStaticVoidMethod(JNIEnv *env, jclass cls, jmethodID methodID, ...))
  va_list args;
  va_start(args, methodID);
  JavaValue jvalue(T_VOID);
 
  JNI_ArgumentPusherVaArg  ap(methodID, args);
  jni_invoke_static(env, &jvalue, NULL, JNI_STATIC, methodID, &ap, CHECK);
  va_end(args);
JNI_END

After the parameters passed to the Java method are passed in as C variable length parameters, JNI is used_ The argumentpushervaarg instance ap encapsulates it. JNI_ The inheritance system of argumentpushervaarg class is as follows:

JNI_ArgumentPusherVaArg->JNI_ArgumentPusher->SignatureIterator

JNI called\_ invoke\_ The static() function is implemented as follows:

// Calling Java static methods through jni
static void jni_invoke_static(
 JNIEnv *env,
 JavaValue* result,
 jobject receiver,
 JNICallType call_type,
 jmethodID method_id,
 JNI_ArgumentPusher *args,
 TRAPS
){
  Method* m = Method::resolve_jmethod_id(method_id);
  methodHandle method(THREAD, m);
 
  ResourceMark rm(THREAD);
  int number_of_parameters = method->size_of_parameters();
  // Here, the parameters to be passed to Java are further converted into JavaCallArguments objects and passed down
  JavaCallArguments java_args(number_of_parameters);
  args->set_java_argument_object(&java_args);
 
  // Fill out JavaCallArguments object
  Fingerprinter fp = Fingerprinter(method);
  uint64_t x = fp.fingerprint();
  args->iterate(x);
  // Initialize result type
  BasicType bt = args->get_ret_type();
  result->set_type(bt);
 
  // Invoke the method. Result is returned as oop.
  JavaCalls::call(result, method, &java_args, CHECK);
 
  // Convert result
  if (
    result->get_type() == T_OBJECT || 
    result->get_type() == T_ARRAY
  ) {
     oop tmp = (oop) result->get_jobject();
     jobject jobj = JNIHandles::make_local(env,tmp);
     result->set_jobject(jobj);
  }
}

Call the main() method of the Java main class through the JavaCalls::call() function. You should not be unfamiliar with the JavaCalls::call() function. How this function establishes Java stack frames and finds Java method entries has been described in detail before, and will not be introduced here.

Part 30 - explain a small example of executing the main() method

After introducing the assembly code execution logic of some common bytecode instructions, we can basically see the whole logic of a main() method from the beginning of calling, stack frame establishment and bytecode execution, but the knowledge points such as method stack withdrawal, synchronization method and exception throwing have not been introduced. We will only give the simplest example here, It can help you review what you have learned in so many previous articles.

The stack frames created for Java methods are described in detail in Chapter 7, as shown in the following figure.

After calling generate\_ fixed\_ The values saved in some registers after the frame() function are as follows:

rbx: Method*
ecx: invocation counter
r13: bcp(byte code pointer)
rdx: ConstantPool* Address of constant pool
r14: Address of the first parameter in the local variable table

Now let's take an example to fully explain the implementation process. This example is as follows:

package com.classloading;
 
public class Test {
    public static void main(String[] args) {
        int i = 0;
        i = i++;
    }
}

The bytecode file decompiled by javap -verbose Test.class command is as follows:

Constant pool:
   #1 = Methodref #3.#12 // java/lang/Object."<init>":()V
   #2 = Class #13 // com/classloading/Test
   #3 = Class #14 // java/lang/Object
   #4 = Utf8 <init>
   #5 = Utf8 ()V
   #6 = Utf8 Code
   #7 = Utf8 LineNumberTable
   #8 = Utf8 main
   #9 = Utf8 ([Ljava/lang/String;)V
  #10 = Utf8 SourceFile
  #11 = Utf8 Test.java
  #12 = NameAndType #4:#5 // "<init>":()V
  #13 = Utf8 com/classloading/Test
  #14 = Utf8 java/lang/Object
{
  ...
 
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: iconst_0
         1: istore_1
         2: return
}

The stack frame status corresponding to the above example is shown in the following figure.

Now let's execute the bytecode in the main() method in an interpreted manner. Because it is called from the virtual machine, generate is called\_ fixed\_ The values stored in some registers after the frame () function do not involve the stack top cache, so you need to retrieve them from iconst\_ Enter the vtos entry of bytecode instruction 0, and then find iconst\_0 is the machine instruction fragment corresponding to the bytecode instruction.

Now review the logic of bytecode dispatch in generate\_ normal\_ Generate is called in the entry() function\_ fixed\_ The frame () function generates the corresponding stack frame for the execution of Java methods, and then calls dispatch_ The next() function executes the bytecode of the Java method. The assembly when obtaining the bytecode for the first time is as follows:

// In generate_ fixed_ The bcp has been stored in% r13 in the frame () method
movzbl 0x0(%r13),%ebx // %Stored in ebx is the opcode of bytecode
  
// $0x7ffff73ba4a0 this address points to a one-dimensional array in the corresponding state, with a length of 256
movabs $0x7ffff73ba4a0,%r10
  
// Note that constants are stored in% r10. Obtain the address to the storage entry address according to the calculation formula% r10+%rbx*8,
// Get the entry address through * (% r10+%rbx*8), and then jump to the entry address for execution
jmpq *(%r10,%rbx,8)

Note that the constant value of $0x7ffff73ba4a0 above indicates the first address of the one-dimensional array under the stack top cache state of vtos. When the bytecode of the method is allocated for the first time, the Opcode corresponding to the bytecode can be retrieved through 0x0(%r13), and this Opcode can be used to locate iconst_ The entry address of 0.

%r10 refers to the one-dimensional array corresponding to the cache state at the top of the stack, with a length of 256, in which the stored value is Opcode, which has been described in detail in Chapter 8. The schematic diagram is shown in the following figure.

 

Now let's look at iconst with vtos as the entrance and itos as the exit_ The assembly code to be executed by 0 is as follows:

...
 
// vtos inlet
mov $0x1,%eax
 
...
// iconst_ Assembly code corresponding to 0
xor    %eax,%eax

The assembly instruction is simple enough. Finally, the value is stored in% eax, so the exit state of the stack top cache is itos.

The purple part in the figure above is the local variable table. Since the size of the local variable table is 2, I drew two squares to represent the slot.

Execute the next bytecode instruction istore_1. The logic related to bytecode dispatch will also be executed. It should be reminded that when introducing the assembly corresponding to bytecode instructions, we only focused on the execution logic of bytecode instructions. In fact, when generating machine instructions for each bytecode instruction, three parts of machine instruction segments are generally generated for these bytecode instructions:

(1) The entry execution logic corresponding to different stack top states;

(2) The logic to be executed by bytecode instruction itself;

(3) The logic assigned to the next bytecode instruction.

In the bytecode instruction template definition, if the instruction in flags has disp, these instructions will contain dispatch logic, such as goto, ireturn, tableswitch, lookupswitch, jsr, etc. Because our instruction is iconst_0, so dispatch logic will be generated for this bytecode instruction. The generated logic is as follows:

movzbl 0x1(%r13),%ebx    // %Stored in ebx is the opcode of bytecode
  
movabs itos The first address of the corresponding one-dimensional array,%r10
 
jmpq *(%r10,%rbx,8)

Note that if you want to store iStore in% ebx\_ 1, then%r13 iconst needs to be added\_ The length of the 0 instruction, i.e. 1. Because iconst\_ The exit stack top cache after 0 execution is itos, so you need to find that the entry status is itos and the Opcode is istore\_1 machine instruction fragment execution. The instruction fragments are as follows:

mov    %eax,-0x8(%r14)

The code stores the value%eax at the top of the stack at the location where the subscript index of the local variable table is 1. By%r14 easily locating the location of the local variable table, the stack status after execution is shown in the following figure.

Execute iconst\_0 and iStore\_ At 1, the whole process does not push 0 into the expression stack (the following part is the expression stack from sp/rsp in the above figure). In fact, if there is no optimization of the top cache, 0 should be pushed into the top of the stack, and then the top of the stack should be popped up and stored in the local variable table. However, with the top cache, there is no stack pressing operation, and there is also a stack bouncing operation, Therefore, it can greatly improve the execution efficiency of the program.

The return instruction has a lot of logic to judge, mainly because some methods may have the synchronized keyword, so the lock related information will be saved in the method stack. When return returns, the lock will be released when returning from the stack. However, we will only look at some of the code to be run for this example, as follows:

// Javathread:: do_ not_ unlock_ if_ The synchronized attribute is stored in% dl
0x00007fffe101b770: mov 0x2ad(%r15),%dl
// Reset javathread:: do_ not_ unlock_ if_ The synchronized property value is false
0x00007fffe101b777: movb $0x0,0x2ad(%r15)
 
// Load Method * into% rbx
0x00007fffe101b77f: mov -0x18(%rbp),%rbx
// Change Method::_access_flags loaded into% ecx
0x00007fffe101b783: mov 0x28(%rbx),%ecx
// Check whether Method::flags contains the JVM_ACC_SYNCHRONIZED
0x00007fffe101b786: test $0x20,%ecx
// If the method is not a synchronous method, jump to - ---- unlocked----
0x00007fffe101b78c: je 0x00007fffe101b970

The main() method is an asynchronous method, so jump to the unlocked place for execution. Some logic for releasing locks will be executed in the logic executed at the unlocked place. This is not important for our example. Let's directly look at the operation of returning the stack, as follows:

// Take out the old stack pointer(saved rsp) saved at - 0x8(%rbp) and put it into% rbx
0x00007fffe101bac7: mov -0x8(%rbp),%rbx
 
// Remove stack frame
// The leave instruction is equivalent to:
// mov %rbp, %rsp
// pop %rbp
0x00007fffe101bacb: leaveq 
// Pop return address into% r13
0x00007fffe101bacc: pop %r13
// Set% rsp as the top stack value of the caller
0x00007fffe101bace: mov %rbx,%rsp
0x00007fffe101bad1: jmpq *%r13

This compilation is not difficult and will not be introduced here. The stack status after stack withdrawal is shown in the following figure.

This completely returns to the stack state before calling Java methods. Next, how to exit the above stack frame and end the method call is a matter of C + + language.

Part 31 - invokevirtual of method call instructions

The template of invokevirtual bytecode instruction is defined as follows:

def(Bytecodes::_invokevirtual , ubcp|disp|clvm|____, vtos, vtos, invokevirtual , f2_byte );

The generating function is invokevirtual, and the parameter passed is f2_byte, that is, 2. If it is 2, the b2 part in [b2,b1,original constant pool index] is taken from constantpoolcacheentry:: indexes. The implementation of the called TemplateTable::invokevirtual() function is as follows:

void TemplateTable::invokevirtual(int byte_no) {
  prepare_invoke(byte_no,
                 rbx, // method or vtable index
                 noreg, // unused itable index
                 rcx, // recv
                 rdx); // flags

  // rbx: index
  // rcx: receiver
  // rdx: flags
  invokevirtual_helper(rbx, rcx, rdx);
}

Call prepare first\_ Invoke () function, after calling invokevirtual\_ The helper () function generates the assembly code corresponding to the invokevirtual bytecode instruction (in fact, it generates the machine instruction, and then decompiles the corresponding assembly code, which will be directly expressed as assembly code later. Readers should know).

1,prepare_invoke() function

Call templatetable:: prepare_ The invoke () function generates a lot of assembly code, so we can view it in three parts.

Part 1:

0x00007fffe1021f90: mov %r13,-0x38(%rbp) // Save bcp to stack
// Retrieve x from invokevirtual x, that is, store the constant pool index in% edx,
// In fact, this is already the index of ConstantPoolCacheEntry, because it is connected to the class
// The phase rewrites some specific bytecode instructions in the method
0x00007fffe1021f94: movzwl 0x1(%r13),%edx 
// Store the first address of ConstantPoolCache in% rcx


0x00007fffe1021f99: mov -0x28(%rbp),%rcx 

// Shift left by 2 bits because the ConstantPoolCacheEntry index is stored in% edx, and shift left by 2 bits because
// ConstantPoolCacheEntry takes 4 words
0x00007fffe1021f9d: shl $0x2,%edx 
       
// Calculate% rcx+%rdx*8+0x10 and obtain the values in constantpoolcacheentry [_indexes, _f1, _f2, _flags]_ indices
// Because the size of ConstantPoolCache is 0x16 bytes,% rcx+0x10 is located
// To the location of the first ConstantPoolCacheEntry
// %rdx*8 calculates the byte offset relative to the first ConstantPoolCacheEntry
0x00007fffe1021fa0: mov 0x10(%rcx,%rdx,8),%ebx 

// Get b2 in indexes [b2, B1, constant pool index] in ConstantPoolCacheEntry
0x00007fffe1021fa4: shr $0x18,%ebx 

// Take out the b2 contained in indices, that is, bytecode, and store it in% ebx
0x00007fffe1021fa7: and $0xff,%ebx 

// Check whether the bytecode of 182 is connected 
0x00007fffe1021fad: cmp $0xb6,%ebx 
 
// If connected, jump to resolved 
0x00007fffe1021fb3: je 0x00007fffe1022052

It is mainly used to check whether the bytecode has been connected. If it has not been connected, it needs to be connected. If it has been connected, it will jump to resolved and directly execute the method call operation.

Part 2:

// Call InterpreterRuntime::resolve_invoke() function, because the instruction is not connected yet
// Move instruction with bytecode 182 to% ebx
0x00007fffe1021fb9: mov $0xb6,%ebx 

// By calling MacroAssembler::call_VM() function to call
// InterpreterRuntime::resolve_invoke(JavaThread* thread, Bytecodes::Code bytecode) function
// Make method connection
0x00007fffe1021fbe: callq 0x00007fffe1021fc8 
0x00007fffe1021fc3: jmpq 0x00007fffe1022046 // Jump to ----- E----
// Prepare the second parameter, bytecode
0x00007fffe1021fc8: mov %rbx,%rsi 
0x00007fffe1021fcb: lea 0x8(%rsp),%rax
0x00007fffe1021fd0: mov %r13,-0x38(%rbp)
0x00007fffe1021fd4: mov %r15,%rdi
0x00007fffe1021fd7: mov %rbp,0x200(%r15)
0x00007fffe1021fde: mov %rax,0x1f0(%r15)
0x00007fffe1021fe5: test $0xf,%esp
0x00007fffe1021feb: je 0x00007fffe1022003
0x00007fffe1021ff1: sub $0x8,%rsp
0x00007fffe1021ff5: callq 0x00007ffff66ac528
0x00007fffe1021ffa: add $0x8,%rsp
0x00007fffe1021ffe: jmpq 0x00007fffe1022008
0x00007fffe1022003: callq 0x00007ffff66ac528
0x00007fffe1022008: movabs $0x0,%r10
0x00007fffe1022012: mov %r10,0x1f0(%r15)
0x00007fffe1022019: movabs $0x0,%r10
0x00007fffe1022023: mov %r10,0x200(%r15)
0x00007fffe102202a: cmpq $0x0,0x8(%r15)
0x00007fffe1022032: je 0x00007fffe102203d
0x00007fffe1022038: jmpq 0x00007fffe1000420
0x00007fffe102203d: mov -0x38(%rbp),%r13
0x00007fffe1022041: mov -0x30(%rbp),%r14
0x00007fffe1022045: retq 
// End MacroAssembler::call_VM() function call

// **** E ****
// Load x from invokevirtual x into% edx, that is, the index of ConstantPoolCacheEntry
0x00007fffe1022046: movzwl 0x1(%r13),%edx 

// Store the first address of ConstantPoolCache in% rcx 
0x00007fffe102204b: mov -0x28(%rbp),%rcx 

 // %The constant poolcacheentry index stored in edx is converted to word offset
0x00007fffe102204f: shl $0x2,%edx

The logic of method connection is similar to that of the field described earlier. Both are to improve the corresponding ConstantPoolCacheEntry in ConstantPoolCache and add relevant information.

Call interpreterruntime:: Resolve_ The invoke () function is used to connect methods. There are many implementations of this function, which we will introduce in detail in the next article. After the connection is completed, the items in the ConstantPoolCacheEntry are shown in the following figure.

Therefore, for invokevirtual, the Method is distributed through vtable in the ConstantPoolCacheEntry\_ The f1 field is not used, but\_ For the field f2, if a non final virtual Method is called, the index number of the target Method in vtable is saved. If it is a virtual final Method, the_ The f2 field points directly to the Method instance of the target Method.

Part 3:

// **** resolved ****

// The definition point of resolved indicates that the invokevirtual bytecode has been connected
// Get constantpoolcacheentry:_ F2, this field is only meaningful for virtual
// During calculation, because the ConstantPoolCacheEntry is saved after the ConstantPoolCache,
// Therefore, the ConstantPoolCache is 0x10, and
// _ Also offset 0x10 so that the total offset is 0x20
// ConstantPoolCacheEntry::_ Store to% rbx
0x00007fffe1022052: mov 0x20(%rcx,%rdx,8),%rbx 
 // ConstantPoolCacheEntry::_flags stored in% edx
0x00007fffe1022057: mov 0x28(%rcx,%rdx,8),%edx 
 // Move flags to ecx
0x00007fffe102205b: mov %edx,%ecx 
// Fetch parameter size from flags 
0x00007fffe102205d: and $0xff,%ecx 

         
// The recv is obtained, and the parameter size is saved in% RCX. The size required for the final calculation of the parameter is% rsp+%rcx*8-0x8,
// The parameter size in flags already includes the size of recv for instance methods
// For example, the first parameter to call the instance method is this(recv)
0x00007fffe1022063: mov -0x8(%rsp,%rcx,8),%rcx // recv saved to% rcx 

// Store flags in r13
0x00007fffe1022068: mov %edx,%r13d 
// Get return type from flags, that is, from_ TosState saved in the upper 4 bits of flags
0x00007fffe102206b: shr $0x1c,%edx 

// Templateinterpreter:: invoke_ return_ The entry address is stored in% r10
0x00007fffe102206e: movabs $0x7ffff73b6380,%r10 
// %rdx saves the return type and calculates the return address
// Because TemplateInterpreter::invoke_return_entry is an array,
// So find the entry address corresponding to return type
0x00007fffe1022078: mov (%r10,%rdx,8),%rdx 
// Push the return address into the stack
0x00007fffe102207c: push %rdx 

// Restore ConstantPoolCacheEntry::_flags 
0x00007fffe102207d: mov %r13d,%edx 
// Restore bcp
0x00007fffe1022080: mov -0x38(%rbp),%r13

TemplateInterpreter::invoke\_return\_entry saves the entry of a routine, which will be described in detail later.

After executing the above code, the relevant value has been stored in the relevant register. The relevant register states are as follows:

rbx: Stored is ConstantPoolCacheEntry::_f2 The value of the property
rcx: Is the first parameter when calling the instance method this
rdx: Stored is ConstantPoolCacheEntry::_flags The value of the property

The status of the stack is shown in the following figure.

Templateinterpreter:: invoke was pushed into the stack\_ return\_ The return address of the entry.  

2,invokevirtual_helper() function

Call templatetable:: invokevirtual_ The code generated by the helper () function is as follows:

// flags stored in% eax
0x00007fffe1022084: mov %edx,%eax 
// Test whether the called method is final 
0x00007fffe1022086: and $0x100000,%eax 
// If it is not final, jump directly to ----- notFinal---- 
0x00007fffe102208c: je 0x00007fffe10220c0 

// Get the receiver value through (% rcx). If% rcx is empty, an OS exception will be caused
0x00007fffe1022092: cmp (%rcx),%rax 

// Omit the code part related to statistics

// Set the top of the caller stack and save
0x00007fffe10220b4: lea 0x8(%rsp),%r13
0x00007fffe10220b9: mov %r13,-0x10(%rbp)

// Jump to method:_ from_ interpretered_ Entry entry to execute
0x00007fffe10220bd: jmpq *0x58(%rbx)

For the final method, there is no dynamic dispatch, so there is no need to find the target through vtable. The stack when calling is shown in the following figure.

The following code is to find the method entry to be called for dynamic dispatch through vtable.

// **** notFinal ****

// If the invokevirtual instruction calls a non final method, you can jump directly here
// %The receiver stored in rcx is represented by oop. Get Klass through oop
0x00007fffe10220c0: mov 0x8(%rcx),%eax 

// Call macroassembler:: decode_ klass__ not_ The null () function generates the following assembly code
0x00007fffe10220c3: shl $0x3,%rax // LogKlassAlignmentInBytes=0x03

// Omit the code part related to statistics


// %Recv is stored in rax_ klass
// %VTable is stored in rbx_ index,
// And 0x1b8 is InstanceKlass::vtable_start_offset()*wordSize+vtableEntry::method_offset_in_bytes(),
// In fact, the Method * to be called is found through dynamic dispatch and stored in% rbx
0x00007fffe1022169: mov 0x1b8(%rax,%rbx,8),%rbx

// Set the top stack address of the caller and save it
0x00007fffe1022171: lea 0x8(%rsp),%r13
0x00007fffe1022176: mov %r13,-0x10(%rbp)

// Jump to method:_ from_ interpreted_ Execute at entry
0x00007fffe102217a: jmpq *0x58(%rbx)

When understanding the above code, you need to know the vtable method assignment and the layout of vtable in InstanceKlass. This is described in detail in the book in-depth analysis of Java virtual machine: source code analysis and example explanation, which will not be introduced here.   

Jump to method:\_ from\_ interpretered\_ Entry is executed at the routine saved, that is, to explain the target method called by running invokevirtual bytecode instructions. About method::\_ from\_ interpretered\_ The logic of the routine saved by entry has been described in detail in Articles 6, 7 and 8, and will not be introduced here.

The above assembly statement mov 0x1b8(%rax,%rbx,8),%rbx calls lookup by calling\_ virtual\_ Generated by method() function, this function will vtable\_entry\_addr is loaded into% rbx. The implementation is as follows:

void MacroAssembler::lookup_virtual_method(Register recv_klass,
                                           RegisterOrConstant vtable_index,
                                           Register method_result) {
  const int base = InstanceKlass::vtable_start_offset() * wordSize;
  Address vtable_entry_addr(
    recv_klass,
    vtable_index,
    Address::times_ptr,
    base + vtableEntry::method_offset_in_bytes());
    movptr(method_result, vtable_entry_addr);
}

Where vtable\_index takes constantpoolcacheentry:\_ The value of the property.

Finally, the statistics related execution logic is omitted from some assembly codes generated above. Here, the statistics related code is also very important. It will assist in compilation, so we will introduce these statistics related logic later.

Part 32 - parsing interfacevirtual bytecode instructions

When the invokevirtual instruction was introduced earlier, if it is judged that the\_ Of the indices field\_ If the value of the attribute is empty, it is considered that the target method called is not connected, that is, the relevant information of the calling method is not saved in the ConstantPoolCacheEntry, so it is necessary to call interpreterruntime:: Resolve_ The invoke() function is used to connect methods. There are many implementations of this function. Let's check it in several parts:

InterpreterRuntime::resolve_invoke() function part 1:

Handle receiver(thread, NULL);
if (bytecode == Bytecodes::_invokevirtual || bytecode == Bytecodes::_invokeinterface) {
    ResourceMark rm(thread);
    // Call the method() function to get the method to be executed from the current stack frame
    Method* m1 = method(thread);
    methodHandle m (thread, m1);
 
    // Call the bci() function to get the bytecode index of the method to be executed from the current stack frame
    int i1 = bci(thread);
    Bytecode_invoke call(m, i1);
 
    // Signature of the method currently to be executed
    Symbol* signature = call.signature();
 
    frame fm = thread->last_frame();
    oop x = fm.interpreter_callee_receiver(signature);
    receiver = Handle(thread,x);
}

When the bytecode is a dynamically dispatched bytecode such as invokevirtual or invokeinterface, the above logic is executed. Got the value of the receiver variable. Next, let's look at the implementation, as follows:

InterpreterRuntime::resolve_invoke() function part 2:

CallInfo info;
constantPoolHandle pool(thread, method(thread)->constants());
 
{
    JvmtiHideSingleStepping jhss(thread);
    int cpcacheindex = get_index_u2_cpcache(thread, bytecode);
    LinkResolver::resolve_invoke(info, receiver, pool,cpcacheindex, bytecode, CHECK);
    ...
} 
 
// If the relevant information of the call has been updated to the ConstantPoolCacheEntry, it is returned directly
if (already_resolved(thread))
  return;

Obtain the operand of bytecode instruction according to the bcp stored in the current stack. This operand is usually the constant pool cache entry index. Then call LinkResolver:: resolve\_ The invoke () function performs method concatenation.   This function will indirectly call LinkResolver::resolve\_invokevirtual() function is implemented as follows:

void LinkResolver::resolve_invokevirtual(
 CallInfo& result,
 Handle recv,
 constantPoolHandle pool,
 int index,
 TRAPS
){
 
  KlassHandle resolved_klass;
  Symbol* method_name = NULL;
  Symbol* method_signature = NULL;
  KlassHandle current_klass;
 
  resolve_pool(resolved_klass, method_name, method_signature, current_klass, pool, index, CHECK);
 
  KlassHandle recvrKlass(THREAD, recv.is_null() ? (Klass*)NULL : recv->klass());
 
  resolve_virtual_call(result, recv, recvrKlass, resolved_klass, method_name, method_signature, current_klass, true, true, CHECK);
}

Where resolve is called\_ Pool () and resolve\_ vritual_ The call () function connects the constant pool and method call instructions respectively. The related functions involved in the call are roughly as shown in the figure below.

Resolve is described below\_ Pool () and resolve\_ virtual_ Implementation of call() function and its related functions.

01 resolve_pool() function

Resolve called_ The pool () function will call some functions, as shown in the following figure.

Every time linkresolver:: resolve is called\_ The pool () function does not necessarily follow the above function call chain, but when the class has not been resolved, it usually calls systemdictionary:: resolve\_ or_ After parsing with the fail() function, you will eventually get the pointer to the Klass instance, and finally update this class to the constant pool.

resolve_ The implementation of the pool() function is as follows:

void LinkResolver::resolve_pool(
 KlassHandle& resolved_klass,
 Symbol*& method_name,
 Symbol*& method_signature,
 KlassHandle& current_klass,
 constantPoolHandle pool,
 int index,
 TRAPS
) {
  resolve_klass(resolved_klass, pool, index, CHECK);
 
  method_name = pool->name_ref_at(index);
  method_signature = pool->signature_ref_at(index);
  current_klass = KlassHandle(THREAD, pool->pool_holder());
}

Where index is the index of the constant pool cache entry. resolved\_ The Klass parameter indicates the class that needs to be parsed (parsing is the connection related part in the class generation cycle, so we sometimes call it connection before. In fact, it specifically means parsing), and current\_klass is a class that currently has a constant pool. Since the parameters are passed by C + + reference, the same value will directly change the value of the variable, and the value in the caller will change with it.

Call resolve_klass() function performs class parsing. Generally speaking, class parsing will be performed when interpreting constant pool items. This is introduced in the book in-depth analysis of Java virtual machine: source code analysis and detailed explanation of examples (basic volume). It needs to be repeated here.

Resolve called_ Klass() function and related functions are implemented as follows:

void LinkResolver::resolve_klass(
 KlassHandle& result,
 constantPoolHandle pool,
 int                  index,
 TRAPS
) {
  Klass* result_oop = pool->klass_ref_at(index, CHECK);
  // Pass by reference
  result = KlassHandle(THREAD, result_oop);
}
 
Klass* ConstantPool::klass_ref_at(int which, TRAPS) {
  int x = klass_ref_index_at(which);
  return klass_at(x, CHECK_NULL);
}
 
int klass_ref_index_at(int which) {
  return impl_klass_ref_index_at(which, false);
}

Called impl\_ klass\_ ref\_ index\_ The implementation of at() function is as follows:

int ConstantPool::impl_klass_ref_index_at(int which, bool uncached) {
  int i = which;
  if (!uncached && cache() != NULL) {
    // Obtain the ConstantPoolIndex from the ConstantPoolCacheEntry item corresponding to which
    i = remap_instruction_operand_from_cache(which);
  }
 
  assert(tag_at(i).is_field_or_method(), "Corrupted constant pool");
  // obtain
  jint ref_index = *int_at_addr(i);
  // Get the lower 16 bits, that is class_index
  return extract_low_short_from_int(ref_index);
}

According to the assertion, the entry at i of the original constant pool index must be a JVM\_CONSTANT\_Fieldref,JVM\_CONSTANT\_Methodref or JVM\_CONSTANT\_InterfaceMethodref. The formats of these items are as follows:

CONSTANT_Fieldref_info{
  u1 tag;
  u2 class_index; 
  u2 name_and_type_index; // Must be a field descriptor
}
 
CONSTANT_InterfaceMethodref_info{
  u1 tag;
  u2 class_index; // Must be an interface
  u2 name_and_type_index; // Must be a method descriptor
}
 
CONSTANT_Methodref_info{
  u1 tag;
  u2 class_index; // Must be a class
  u2 name_and_type_index; // Must be a method descriptor
}

The formats of the three items are the same, including class\_ The entry at index must be CONSTANT\_Class\_info structure, which represents a class or interface. The current class field or method is a member of this class or interface. name\_ and\_ type\_ Must be constant at index\_ NameAndType\_ Info item.   

By calling int\_at\_addr() function and extract\_low\_short\_from\_int() function to get class_ The index value of index. If you understand the memory layout of the constant pool, the implementation of the function here will be very simple, which will not be introduced here.

In Klass\_ ref\_ Call klass_ in at () function At() function. The implementation of this function is as follows:

Klass* klass_at(int which, TRAPS) {
    constantPoolHandle h_this(THREAD, this);
    return klass_at_impl(h_this, which, CHECK_NULL);
}

Klass called\_ at\_ The impl() function is implemented as follows:

Klass* ConstantPool::klass_at_impl(
 constantPoolHandle this_oop,
 int which,
 TRAPS
) {
   
  CPSlot entry = this_oop->slot_at(which);
  if (entry.is_resolved()) { // Already connected
    return entry.get_klass();
  }
 
  bool do_resolve = false;
  bool in_error = false;
 
  Handle mirror_handle;
  Symbol* name = NULL;
  Handle loader;
  {
     MonitorLockerEx ml(this_oop->lock());
 
    if (this_oop->tag_at(which).is_unresolved_klass()) {
      if (this_oop->tag_at(which).is_unresolved_klass_in_error()) {
        in_error = true;
      } else {
        do_resolve = true;
        name = this_oop->unresolved_klass_at(which);
        loader = Handle(THREAD, this_oop->pool_holder()->class_loader());
      }
    }
  } // unlocking constantPool
 
  // Omit when in_ Processing logic when the value of the error variable is true
  
  if (do_resolve) {
    oop protection_domain = this_oop->pool_holder()->protection_domain();
    Handle h_prot (THREAD, protection_domain);
    Klass* k_oop = SystemDictionary::resolve_or_fail(name, loader, h_prot, true, THREAD);
    KlassHandle k;
    if (!HAS_PENDING_EXCEPTION) {
      k = KlassHandle(THREAD, k_oop);
      mirror_handle = Handle(THREAD, k_oop->java_mirror());
    }
 
    if (HAS_PENDING_EXCEPTION) {
      ...
      return 0;
    }
 
    if (TraceClassResolution && !k()->oop_is_array()) {
      ... 
    } else {
      MonitorLockerEx ml(this_oop->lock());
      do_resolve = this_oop->tag_at(which).is_unresolved_klass();
      if (do_resolve) {
        ClassLoaderData* this_key = this_oop->pool_holder()->class_loader_data();
        this_key->record_dependency(k(), CHECK_NULL); // Can throw OOM
        this_oop->klass_at_put(which, k()); // Note that the contents stored in the constant pool will be updated here, which means that the class has been parsed and there is no need to repeat the parsing next time
      }
    }
  }
 
  entry = this_oop->resolved_klass_at(which);
  assert(entry.is_resolved() && entry.get_klass()->is_klass(), "must be resolved at this point");
  return entry.get_klass();
}

The function first calls slot\_ The at() function obtains the value stored in a slot in the constant pool, and then represents the slot through CPSlot. There are two possible values stored in the slot, namely, the pointer to the Symbol instance (because the class name is represented by the CONSTANT\_Utf8_info item, and the Symbol object is used to represent the string in the virtual machine). If the class has been explained, Then the last bit of the address represented by the pointer is 0. If it has not been resolved, the last bit of the address is 1.

When there is no resolution, you need to call systemdictionary:: resolve\_ or\_ The fail () function obtains the instance of class Klass, and then updates the information in the constant pool, so that the class does not need to be parsed again next time. Finally, return the pointer to the Klass instance.

Continue back to linkresolver:: resolve\_ The pool () function looks at the next execution logic, that is, it will get the JVM\_CONSTANT\_Fieldref,JVM\_CONSTANT\_Methodref or JVM\_ CONSTANT\_ Name in interfacemethodref entry\_ and\_ type\_ Index, which points to CONSTANT\_NameAndType\_info item in the following format:

CONSTANT_NameAndType_info{
   u1 tag;
  u2 name_index;
  u2 descriptor index;
}

The acquisition logic is to find the index of the original constant pool item according to the index of the constant pool cache item, and then find the CONSTANT\_NameAndType\_info, get the index of the method name and signature, and then get the name and signature of the called target method. This information will be called in the next resolve\_ virtual\_ Used in the call() function.

02 resolve\_virtual\_call() function

resolve\_ virtual\_ The related functions that the call() function will call are shown in the following figure.

LinkResolver::resolve\_ virtual\_ The implementation of call() is as follows:

void LinkResolver::resolve_virtual_call(
 CallInfo& result,
 Handle recv,
 KlassHandle receiver_klass,
 KlassHandle resolved_klass,
 Symbol* method_name,
 Symbol* method_signature,
 KlassHandle current_klass,
 bool         check_access,
 bool         check_null_and_abstract,
 TRAPS
) {
  methodHandle resolved_method;
 
  linktime_resolve_virtual_method(resolved_method, resolved_klass, method_name, method_signature, current_klass, check_access, CHECK);
 
  runtime_resolve_virtual_method(result, resolved_method, resolved_klass, recv, receiver_klass, check_null_and_abstract, CHECK);
}

First call LinkResolver::linktime\_resolve\_virtual_method() function, which will call the following functions:

void LinkResolver::resolve_method(
 methodHandle& resolved_method,
 KlassHandle resolved_klass,
 Symbol* method_name,
 Symbol* method_signature,
 KlassHandle current_klass,
 bool          check_access,
 bool          require_methodref,
 TRAPS
) {
 
  // Find a method from the resolved class and its parent class
  lookup_method_in_klasses(resolved_method, resolved_klass, method_name, method_signature, true, false, CHECK);
 
  // No method was found in the inheritance system of the resolved class
  if (resolved_method.is_null()) { 
    // Find methods from all interfaces implemented by the parsing class (including indirectly implemented interfaces)
    lookup_method_in_interfaces(resolved_method, resolved_klass, method_name, method_signature, CHECK);
    // ...
 
    if (resolved_method.is_null()) {
      // No corresponding method found
      ...
    }
  }
 
  // ...
}

The most important of the above functions is based on method\_name and method\_signature from resolved\_ Find the appropriate method in Klass class, and assign it to resolved if found\_ Method variable.

Call lookup\_method\_in\_klasses(),lookup\_method\_in\_interfaces() and other functions to find methods, which will not be introduced here for the time being.

Next, let's look at runtime\_resolve\_virtual_method() function. The implementation of this function is as follows:

void LinkResolver::runtime_resolve_virtual_method(
 CallInfo& result,
 methodHandle resolved_method,
 KlassHandle resolved_klass,
 Handle recv,
 KlassHandle recv_klass,
 bool check_null_and_abstract,
 TRAPS
) {
 
  int vtable_index = Method::invalid_vtable_index;
  methodHandle selected_method;
 
  // When a method is defined in an interface, it represents a miranda method
  if (resolved_method->method_holder()->is_interface()) { 
    vtable_index = vtable_index_of_interface_method(resolved_klass,resolved_method);
 
    InstanceKlass* inst = InstanceKlass::cast(recv_klass());
    selected_method = methodHandle(THREAD, inst->method_at_vtable(vtable_index));
  } else {
    // If you follow the following code logic, it means resolved_method is not a miranda method. It needs dynamic dispatch and must have the correct vtable index
    vtable_index = resolved_method->vtable_index();
 
    // Although some methods seem to need dynamic dispatch, if the method has a final keyword, it can be statically bound, so it can be called directly
    // The final method will not be put in vtable unless it overrides the method in the parent class
    if (vtable_index == Method::nonvirtual_vtable_index) {
      selected_method = resolved_method;
    } else {
      // According to vtable and vtable_index and inst for dynamic dispatch of methods
      InstanceKlass* inst = (InstanceKlass*)recv_klass();
      selected_method = methodHandle(THREAD, inst->method_at_vtable(vtable_index));
    }
  } 
  
  // The type of setup result resolve is CallInfo, and the related information after connection is set for CallInfo
  result.set_virtual(resolved_klass, recv_klass, resolved_method, selected_method, vtable_index, CHECK);
}

When it is a miranda method, linkresolver:: vtable is called\_ index\_ of\_ interface\_ Method() function search; When it is a final method, because the final method cannot be overridden by subclasses, it is resolved\_method is the target calling method; After removing the previous two cases, the remaining methods need to combine vtable and vtable\_index is dynamically dispatched.

The above function will find all the information needed during the call and store it in the result variable of CallInfo type.  

After all the information at the time of the call is obtained and stored in CallInfo, you can fill in the ConstantPoolCacheEntry according to the relevant information in info. Let's go back to interpreter Runtime:: resolve_ The execution logic of the invoke() function.

InterpreterRuntime::resolve_invoke() function part 2:

switch (info.call_kind()) {
  case CallInfo::direct_call: // Direct call
    cache_entry(thread)->set_direct_call(
          bytecode,
          info.resolved_method());
    break;
  case CallInfo::vtable_call: // vtable dispatch
    cache_entry(thread)->set_vtable_call(
          bytecode,
          info.resolved_method(),
          info.vtable_index());
    break;
  case CallInfo::itable_call: // itable dispatch
    cache_entry(thread)->set_itable_call(
          bytecode,
          info.resolved_method(),
          info.itable_index());
    break;
  default: ShouldNotReachHere();
}

Whether it is called directly or dynamically dispatched by VTable and itable, the relevant information will be stored in the constant pool cache entry after the method parsing is completed. Call cache\_ The entry () function gets the corresponding ConstantPoolCacheEntry entry and then calls set\_. vtable_ Call() function, which will call the following functions to update the information in the constantpoolcacheentry item, as follows:

void ConstantPoolCacheEntry::set_direct_or_vtable_call(
 Bytecodes::Code invoke_code,
 methodHandle method,
 int vtable_index
) {
  bool is_vtable_call = (vtable_index >= 0); // FIXME: split this method on this boolean
  
  int byte_no = -1;
  bool change_to_virtual = false;
 
  switch (invoke_code) {
    case Bytecodes::_invokeinterface:
       change_to_virtual = true;
 
    // ...
    // As you can see, through_ invokevirtual instructions are not necessarily distributed dynamically, but may also be statically bound
    case Bytecodes::_invokevirtual: // It is already in the ConstantPoolCacheEntry class
      {
        if (!is_vtable_call) {
          assert(method->can_be_statically_bound(), "");
          // set_f2_as_vfinal_method checks if is_vfinal flag is true.
          set_method_flags(as_TosState(method->result_type()),
                           ( 1      << is_vfinal_shift) |
                           ((method->is_final_method() ? 1 : 0) << is_final_shift) |
                           ((change_to_virtual ? 1 : 0) << is_forced_virtual_shift), // Call the method defined in Object in the interface.
                           method()->size_of_parameters());
          set_f2_as_vfinal_method(method());
        } else {
          // When executing the logic here, if the presentation method is a non statically bound non final method and dynamic dispatch is required, VTable_ The value of index must be greater than or equal to 0
          set_method_flags(as_TosState(method->result_type()),
                           ((change_to_virtual ? 1 : 0) << is_forced_virtual_shift),
                           method()->size_of_parameters());
          // For dynamic distribution, constantpoolcacheentry:_ VTable is saved in F2_ index
          set_f2(vtable_index);
        }
        byte_no = 2;
        break;
      }
      // ...
  }
 
  if (byte_no == 1) {
    // invoke_code is a non invokevirtual and non invokeinterface bytecode instruction
    set_bytecode_1(invoke_code);
  } else if (byte_no == 2) {
    if (change_to_virtual) {
      if (method->is_public()) 
         set_bytecode_1(invoke_code);
    } else {
      assert(invoke_code == Bytecodes::_invokevirtual, "");
    }
    // set up for invokevirtual, even if linking for invokeinterface also:
    set_bytecode_2(Bytecodes::_invokevirtual);
  } 
}

After the connection is completed, the items in the ConstantPoolCacheEntry are shown in the following figure.

Therefore, for invokevirtual, the Method is distributed through vtable in the ConstantPoolCacheEntry\_ The f1 field is not used, but\_ For the field f2, if a non final virtual Method is called, the index number of the target Method in vtable is saved. If it is a virtual final Method, the_ The f2 field points directly to the Method instance of the target Method.

Part 33 - invokeinterface of method call instructions

The template of invokevirtual bytecode instruction is defined as follows:  

def(Bytecodes::_invokeinterface , ubcp|disp|clvm|____, vtos, vtos, invokeinterface , f1_byte );

You can see that the generation function of the instruction is TemplateTable::invokeinterface(). In this function, templatetable:: prepare will be called first\_ Invoke() function, templatetable:: prepare\_ The assembly code generated by the invoke() function is as follows:

Part 1

0x00007fffe1022610: mov %r13,-0x38(%rbp)
0x00007fffe1022614: movzwl 0x1(%r13),%edx
0x00007fffe1022619: mov -0x28(%rbp),%rcx
0x00007fffe102261d: shl $0x2,%edx
// Get the information in constantpoolcacheentry [_indexes, _f1, _f2, _flags]_ indices
0x00007fffe1022620: mov 0x10(%rcx,%rdx,8),%ebx
 
 
// Get b1 in indexes [B2, b1, constant pool index] in ConstantPoolCacheEntry
// If it is connected, b1 should be equal to 185, that is, the opcode of the invokeinterface instruction
0x00007fffe1022624: shr $0x10,%ebx
0x00007fffe1022627: and $0xff,%ebx
0x00007fffe102262d: cmp $0xb9,%ebx
// If the invokeinterface is already connected, jump to - ---- resolved----
0x00007fffe1022633: je 0x00007fffe10226d2

The judgment logic of assembly code is consistent with invokevirtual, which will not be explained too much here.

Part 2

Since the method has not been resolved, you need to set the information in the ConstantPoolCacheEntry so that you don't need to find the call related information again. The resulting assembly is as follows:

// When the following assembly code is executed, it means that the invokeinterface instruction is not connected, that is, in the ConstantPoolCacheEntry
// The call related information has not been saved
   
// By calling call_ The VM () function generates the following assemblies through which
// Call InterpreterRuntime::resolve_invoke() function
// Store bytecode in% ebx
0x00007fffe1022639: mov $0xb9,%ebx 
// Via MacroAssembler::call_VM() to call InterpreterRuntime::resolve_invoke()
0x00007fffe102263e: callq 0x00007fffe1022648 
0x00007fffe1022643: jmpq 0x00007fffe10226c6
0x00007fffe1022648: mov %rbx,%rsi
0x00007fffe102264b: lea 0x8(%rsp),%rax
0x00007fffe1022650: mov %r13,-0x38(%rbp)
0x00007fffe1022654: mov %r15,%rdi
0x00007fffe1022657: mov %rbp,0x200(%r15)
0x00007fffe102265e: mov %rax,0x1f0(%r15)
0x00007fffe1022665: test $0xf,%esp
0x00007fffe102266b: je 0x00007fffe1022683
0x00007fffe1022671: sub $0x8,%rsp
0x00007fffe1022675: callq 0x00007ffff66ae13a
0x00007fffe102267a: add $0x8,%rsp
0x00007fffe102267e: jmpq 0x00007fffe1022688
0x00007fffe1022683: callq 0x00007ffff66ae13a
0x00007fffe1022688: movabs $0x0,%r10
0x00007fffe1022692: mov %r10,0x1f0(%r15)
0x00007fffe1022699: movabs $0x0,%r10
0x00007fffe10226a3: mov %r10,0x200(%r15)
0x00007fffe10226aa: cmpq $0x0,0x8(%r15)
0x00007fffe10226b2: je 0x00007fffe10226bd
0x00007fffe10226b8: jmpq 0x00007fffe1000420
0x00007fffe10226bd: mov -0x38(%rbp),%r13
0x00007fffe10226c1: mov -0x30(%rbp),%r14
0x00007fffe10226c5: retq 
 
// End MacroAssembler::call_VM() function
// Load x from invokeinterface x into% edx
0x00007fffe10226c6: movzwl 0x1(%r13),%edx
// Store the first address of ConstantPoolCache in% rcx
0x00007fffe10226cb: mov -0x28(%rbp),%rcx
// %Stored in edx is the index of the ConstantPoolCacheEntry entry, which is converted to bytes
// Offset, because a ConstantPoolCacheEntry item takes up 4 words
0x00007fffe10226cf: shl $0x2,%edx

Similar to the implementation of invokevirtual, interpreter Runtime:: resolve is still called when the method is not interpreted\_ The invoke () function performs method parsing. We will also introduce interpreter Runtime:: resolve in detail later\_ Implementation of the invoke() function.

After calling resolve\_ After the invoke () function, the information of the call will be stored in the CallInfo instance. So in the called interpreter Runtime:: Resolve\_ The final implementation of the invoke() function is as follows:

switch (info.call_kind()) {
  case CallInfo::direct_call: // Direct call
    cache_entry(thread)->set_direct_call(
          bytecode,
          info.resolved_method());
    break;
  case CallInfo::vtable_call: // vtable dispatch
    cache_entry(thread)->set_vtable_call(
          bytecode,
          info.resolved_method(),
          info.vtable_index());
    break;
  case CallInfo::itable_call: // itable dispatch
    cache_entry(thread)->set_itable_call(
          bytecode,
          info.resolved_method(),
          info.itable_index());
    break;
  default: ShouldNotReachHere();
}

vtable dispatch has been introduced before. Now let's take a look at itable dispatch.

When itable is dispatched, set is called\_ itable\_ The call() function sets the relevant information in the ConstantPoolCacheEntry. The implementation of this function is as follows:

void ConstantPoolCacheEntry::set_itable_call(
 Bytecodes::Code invoke_code,
 methodHandle method,
 int index
) {
 
  InstanceKlass* interf = method->method_holder();
  // interf must be an interface and method must be a non final method
  set_f1(interf); // For itable, then_ f1 is InstanceKlass
  set_f2(index);
  set_method_flags(as_TosState(method->result_type()),
                   0, // no option bits
                   method()->size_of_parameters());
  set_bytecode_1(Bytecodes::_invokeinterface);
}

The information stored in the ConstantPoolCacheEntry is:

  • bytecode stored in_ f2 field, so that when this field has a value, it means that the resolution of this method has been completed;
  • \_ The f1 field stores the interface class that declares the method, that is\_ f1 is a pointer to the Klass instance representing the interface;
  • \_ f2 indicates\_ The index in the Method table corresponding to the f1 interface class. If it is a final Method, it stores a pointer to the Method instance.

After parsing, the items in the ConstantPoolCacheEntry are shown in the following figure.

Part 3

If the invokeinterface bytecode instruction has been parsed, jump directly to resolved for execution; otherwise, call resolve_invoke performs parsing. After parsing, the logic at resolved will be executed, as follows:

// **** resolved ****
// The resolved definition point indicates that the invokeinterface bytecode has been connected
 
 
// After the above assembly, the register values are as follows:
// %edx: ConstantPoolCacheEntry index
// %rcx: ConstantPoolCache
 
// Get constantpoolcacheentry:_ f1
// During calculation, because the ConstantPoolCacheEntry is in the ConstantPoolCache
// Save later, so the ConstantPoolCache is 0x10, and
// _ f1 is also offset by 0x8, so the total offset is 0x18
0x00007fffe10226d2: mov 0x18(%rcx,%rdx,8),%rax 
// Get constantpoolcacheentry:_ Properties
0x00007fffe10226d7: mov 0x20(%rcx,%rdx,8),%rbx
// Get constantpoolcacheentry:_ Flags attribute
0x00007fffe10226dc: mov 0x28(%rcx,%rdx,8),%edx
 
 
// After the above assembly, the register values are as follows:
// %rax: ConstantPoolCacheEntry::_f1
// %rbx: ConstantPoolCacheEntry::_f2
// %edx: ConstantPoolCacheEntry::_flags
 
// Move flags to ecx
0x00007fffe10226e0: mov %edx,%ecx
// From constantpoolcacheentry::_ Get parameter size from flags
0x00007fffe10226e2: and $0xff,%ecx 
// Let% rcx point to recv 
0x00007fffe10226e8: mov -0x8(%rsp,%rcx,8),%rcx 
// Save constantpoolcacheentry temporarily with% r13d::_ Flags attribute
0x00007fffe10226ed: mov %edx,%r13d 
// From_ Get the method return type from the TosState saved in the upper 4 bits of flags 
0x00007fffe10226f0: shr $0x1c,%edx
// Templateinterpreter:: invoke_ return_ The entry address is stored in% r10
0x00007fffe10226f3: movabs $0x7ffff73b63e0,%r10
// %rdx stores the method return type and calculates the return address
// Because TemplateInterpreter::invoke_return_entry is an array,
// So find the entry address corresponding to return type
0x00007fffe10226fd: mov (%r10,%rdx,8),%rdx
// Get result handler function templateinterpreter:: invoke_ return_ The address of the entry and push it into the stack
0x00007fffe1022701: push %rdx 
 
// Recover constantpoolcacheentry::_ % edx in flags
0x00007fffe1022702: mov %r13d,%edx 
// Restore bcp 
0x00007fffe1022705: mov -0x38(%rbp),%r13

Prepare will be called first in the TemplateTable::invokeinterface() function_ Invoke () function, which generates the above assembly. After calling, the values of each register are as follows:

rax: interface klass (from f1)
rbx: itable index (from f2)
rcx: receiver
rdx: flags

Then, execute the assembly fragment generated by the TemplateTable::invokeinterface() function, as follows:

Part 4

// Set constantpoolcacheentry::_ The value of flags is stored in% r14d
0x00007fffe1022709: mov %edx,%r14d
// Check it out_ Does flags contain is_forced_virtual_shift logo, if any,
// It means that the method in the Object class is called and needs to be dynamically dispatched through vtable
0x00007fffe102270c: and $0x800000,%r14d
0x00007fffe1022713: je 0x00007fffe1022812 // Jump to - notMethod----
 
// ConstantPoolCacheEntry::_flags stored in% eax
0x00007fffe1022719: mov %edx,%eax
// Test whether the called method is final
0x00007fffe102271b: and $0x100000,%eax
0x00007fffe1022721: je 0x00007fffe1022755 // If it is a non final method, jump to ----- notFinal----
 
 
// The following assembly code deals with the final method
 
// For the final Method, rbx stores Method *, that is, constantpoolcacheentry:_ Point to Method*
// Jump to method:: from_ Execute at interpreted
0x00007fffe1022727: cmp (%rcx),%rax
// ... omit statistics related code
// Set the top of the caller stack and store it
0x00007fffe102274e: mov %r13,-0x10(%rbp)
// Jump to method:_ from_ interpreted_ entry
0x00007fffe1022752: jmpq *0x58(%rbx) // Call the final method
  
// **** notFinal ****
 
// Call load_ The klass() function generates the following two sentence assembly
// View the Klass corresponding to the oop of recv and store it in% eax
0x00007fffe1022755: mov 0x8(%rcx),%eax 
// Call decode_ klass_ not_ Assembly generated by null() function 
0x00007fffe1022758: shl $0x3,%rax 
 
       
// Omit statistics related codes
 
// Call lookup_ virtual_ The method () function generates the following assembly
0x00007fffe10227fe: mov 0x1b8(%rax,%rbx,8),%rbx
 
// Set the top of the caller stack and store it
0x00007fffe1022806: lea 0x8(%rsp),%r13
0x00007fffe102280b: mov %r13,-0x10(%rbp)
 
// Jump to method:_ from_ interpreted_ entry
0x00007fffe102280f: jmpq *0x58(%rbx)

The above assembly contains the dispatch logic for final and non final methods. For the final Method, because constantpoolcacheentry::_ Stored in F2 is the Method instance to be called, so it is very simple; For non final methods, dynamic dispatch needs to be implemented through vtable. The key assembly statement of the dispatch is as follows:

mov    0x1b8(%rax,%rbx,8),%rbx

It should be noted that only a small number of methods may use this logic to dynamically dispatch vtable, such as calling methods in the Object class.

If you jump to notMethod, you need to dynamically dispatch methods through itable. Let's take a look at the implementation logic of this part:

Part 5

// **** notMethod ****
 
// Let%r14 point to local variable table
0x00007fffe1022812: mov -0x30(%rbp),%r14 
// %receiver is stored in rcx and Klass is stored in edx
0x00007fffe1022816: mov 0x8(%rcx),%edx 
// LogKlassAlignmentInBytes=0x03, align
0x00007fffe1022819: shl $0x3,%rdx
 
// The following code is generated by calling the following function:
__ lookup_interface_method(rdx, // inputs: rec. class
rax, // inputs: interface
rbx, // inputs: itable index
rbx, // outputs: method
r13, // outputs: scan temp. reg
no_such_interface);
 
  
// Get the starting address of vtable 
// %recv.Klass is stored in rdx and is obtained from Klass
// vtable_ The value of the length property
0x00007fffe10228c1: mov 0x118(%rdx),%r13d 
 
// %rdx: recv.Klass,% r13 is vtable_length,
// Finally r13 points to the first itableOffsetEntry
// Add a constant 0x1b8 because vtable was InstanceKlass before
0x00007fffe10228c8: lea 0x1b8(%rdx,%r13,8),%r13 
0x00007fffe10228d0: lea (%rdx,%rbx,8),%rdx 
 
// Get itableoffsetentry:_ Interface and compare with% rax. The interface to be found is stored in% rax
0x00007fffe10228d4: mov 0x0(%r13),%rbx
0x00007fffe10228d8: cmp %rbx,%rax
// If they are equal, jump directly to ---- found_method ----
0x00007fffe10228db: je 0x00007fffe10228f3
 
// **** search ****
// Check whether the value in% rbx is NULL. If it is NULL,
// That means the receiver does not implement the interface to query
0x00007fffe10228dd: test %rbx,%rbx
// Jump to -- L_no_such_interface ----
0x00007fffe10228e0: je 0x00007fffe1022a8c
0x00007fffe10228e6: add $0x10,%r13
 
0x00007fffe10228ea: mov 0x0(%r13),%rbx
0x00007fffe10228ee: cmp %rbx,%rax
// If the interface class is still not found in itableOffsetEntry,
// Then jump to search to continue searching
0x00007fffe10228f1: jne 0x00007fffe10228dd // Jump to - search----
 
// **** found_method ****
 
// Found itableOffsetEntry matching interface, get
// The offset attribute of itableOffsetEntry and store it in% r13d
0x00007fffe10228f3: mov 0x8(%r13),%r13d
// Via recv_klass finds the declaration under this interface after offsetting
// The starting position of a series of methods
0x00007fffe10228f7: mov (%rdx,%r13,1),%rbx

We need to focus on itable's dispatch logic. First, the following assembly is generated:

mov    0x118(%rdx),%r13d

%recv.Klass is stored in rdx, and vtable in Klass is obtained_ The value of the length attribute. With this value, we can calculate the size of vtable and thus the starting address of itable.

The following compilation was then carried out:  

lea    0x1b8(%rdx,%r13,8),%r13

0x1b8 represents the distance from the first address of recv.Klass to vtable, so the final% r13 points to the first address of itable. As shown in the figure below.

 

Later, we can start the loop to find the matching interface from itableOffsetEntry. If found, jump to found\_method, in found\_ In method, find the offset of the corresponding itableOffsetEntry. This offset indicates the offset of the storage location of the method defined in the interface relative to Klass, that is, find the first itableMethodEntry corresponding to the interface. Because the itable index has been stored in%rbx, you can directly locate the corresponding itableMethodEntry according to this index, Let's now take a look at the following two compilations:

lea    (%rdx,%rbx,8),%rdx 
...
mov    (%rdx,%r13,1),%rbx

When executing the second assembly above,% r13 stores the offset relative to the Klass instance, while% rdx stores the Klass first address when executing the first assembly, and then adds the offset relative to the first itableMethodEntry according to the itable index, so as to find the corresponding itableMethodEntry.   

Part 6

When the following assembly is executed, the values of each register are as follows:

rbx: Method* to call
rcx: receiver

The generated assembly code is as follows:

0x00007fffe10228fb: test %rbx,%rbx
// If the% rbx that should have stored Method * is empty, it means that it is not found
// This method, jump to ---- no_such_method ----
0x00007fffe10228fe: je 0x00007fffe1022987 
 
// Saves the caller's stack top pointer
0x00007fffe1022904: lea 0x8(%rsp),%r13 
0x00007fffe1022909: mov %r13,-0x10(%rbp)
// Jump to Method::from_interpreted points to the routine and executes it
0x00007fffe102290d: jmpq *0x58(%rbx) 
 
 
// Omit should_ not_ reach_ Assembly generated by the here() function
 
 
// **** no_such_method ****
// When no method is found, it will jump here for execution
 
// Pop up call prepare_ The return address pushed in by the invoke() function
0x00007fffe1022987: pop %rbx
// Restore let% r13 point to bcp
0x00007fffe1022988: mov -0x38(%rbp),%r13
// Restore to%r14 point to local variable table
0x00007fffe102298c: mov -0x30(%rbp),%r14
 
 
// ... omit call_ The assembly generated by vm() function calls InterpreterRuntime::throw_abstractMethodError() function
// ... omit the call should_ not_ reach_ Assembly code generated by the here() function
 
// **** no_such_interface ****
 
// Assembly code executed when no matching interface is found
0x00007fffe1022a8c: pop %rbx
0x00007fffe1022a8d: mov -0x38(%rbp),%r13
0x00007fffe1022a91: mov -0x30(%rbp),%r14
 
// ... omit call_ The assembly code generated by vm() function calls InterpreterRuntime::throw_IncompatibleClassChangeError() function
// ... omit the call should_ not_ reach_ Assembly code generated by the here() function

The handling of some exceptions is not introduced here. If you are interested, you can take a look at the implementation of relevant assembly code.  

Due to the word limit, "virtual machine interpretation and execution of Java methods (Part 2)" will be released in the next article

There is a performance problem, please find HeapDump performance community

Posted by joel24 on Fri, 03 Dec 2021 07:51:24 -0800