本文由 HeapDump 性能社区首席讲师鸠摩(马智)受权整顿公布
第 1 篇 - 对于 Java 虚拟机 HotSpot,开篇说的简略点
开讲 Java 运行时,这一篇讲一些简略的内容。咱们写的主类中的 main()办法是如何被 Java 虚拟机调用到的?在 Java 类中的一些办法会被由 C /C++ 编写的 HotSpot 虚拟机的 C /C++ 函数调用,不过因为 Java 办法与 C /C++ 函数的调用约定不同,所以并不能间接调用,须要 JavaCalls::call()这个函数辅助调用。(我把由 C /C++ 编写的叫函数,把 Java 编写的叫办法,后续也会延用这样的叫法)如下图所示。
从 C /C++ 函数中调用的一些 Java 办法次要有:
(1)Java 主类中的 main()办法;
(2)Java 主类装载时,调用 JavaCalls::call()函数执行 checkAndLoadMain()办法;
(3)类的初始化过程中,调用 JavaCalls::call()函数执行的 Java 类初始化办法,能够查看 JavaCalls::call_default_constructor()函数,有对办法的调用逻辑;
(4)咱们先省略 main 办法的执行流程(其实 main 办法的执行也是先启动一个 JavaMain 线程,套路都是一样的),单看某个 JavaThread 的启动过程。JavaThread 的启动最终都要通过一个 native 办法 java.lang.Thread#start0()办法实现的,这个办法通过解释器的 native_entry 入口,调用到了 JVM_StartThread()函数。其中的 static void thread_entry(JavaThread* thread, TRAPS)函数中会调用 JavaCalls::call_virtual()函数。JavaThread 最终会通过 JavaCalls::call_virtual()函数来调用字节码中的 run()办法;
(5)在 SystemDictionary::load_instance_class()这个能体现双亲委派的函数中,如果类加载器对象不为空,则会调用这个类加载器的 loadClass()函数(通过 call_virtual()函数来调用)来加载类。
当然还会有其它办法,这里就不一一列举了。通过 JavaCalls::call()、JavaCalls::call_helper()等函数调用 Java 办法,这些函数定义在 JavaCalls 类中,这个类的定义如下:
从 C /C++ 函数中调用的一些 Java 办法次要有:
(1)Java 主类中的 main()办法;
(2)Java 主类装载时,调用 JavaCalls::call()函数执行 checkAndLoadMain()办法;
(3)类的初始化过程中,调用 JavaCalls::call()函数执行的 Java 类初始化办法,能够查看 JavaCalls::call_default_constructor()函数,有对办法的调用逻辑;
(4)咱们先省略 main 办法的执行流程(其实 main 办法的执行也是先启动一个 JavaMain 线程,套路都是一样的),单看某个 JavaThread 的启动过程。JavaThread 的启动最终都要通过一个 native 办法 java.lang.Thread#start0()办法实现的,这个办法通过解释器的 native_entry 入口,调用到了 JVM_StartThread()函数。其中的 static void thread_entry(JavaThread* thread, TRAPS)函数中会调用 JavaCalls::call_virtual()函数。JavaThread 最终会通过 JavaCalls::call_virtual()函数来调用字节码中的 run()办法;
(5)在 SystemDictionary::load_instance_class()这个能体现双亲委派的函数中,如果类加载器对象不为空,则会调用这个类加载器的 loadClass()函数(通过 call_virtual()函数来调用)来加载类。
当然还会有其它办法,这里就不一一列举了。通过 JavaCalls::call()、JavaCalls::call_helper()等函数调用 Java 办法,这些函数定义在 JavaCalls 类中,这个类的定义如下:
源代码地位: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);
};
如上的函数都是自解释的,通过名称咱们就能看出这些函数的作用。其中 JavaCalls::call()函数是更低一层的通用接口。Java 虚拟机标准定义的字节码指令共有 5 个,别离为 invokestatic、invokedynamic、invokestatic、invokespecial、invokevirtual 几种办法调用指令。这些 call_static()、call_virtual()函数外部调用了 call()函数。这一节咱们先不介绍各个办法的具体实现。下一篇将具体介绍。
咱们选一个重要的 main()办法来查看具体的调用逻辑。如下根本照搬 R 大的内容,不过我略做了一些批改,如下:
假如咱们的 Java 主类的类名为 JavaMainClass,上面为了辨别 java launcher 里 C /C++ 的 main()与 Java 层程序里的 main(),把后者写作 JavaMainClass.main()办法。
从刚进入 C /C++ 的 main()函数开始:
启动并调用 HotSpot 虚拟机的 main()函数的线程执行的次要逻辑如下:
main()
-> //... 做一些参数查看
-> //... 开启新线程作为 main 线程,让它从 JavaMain()函数开始执行;该线程期待 main 线程执行完结
在如上线程中会启动另外一个线程执行 JavaMain()函数,如下:
JavaMain()
-> //... 找到指定的 JVM
-> //... 加载并初始化 JVM
-> //... 依据 Main-Class 指定的类名加载 JavaMainClass
-> //... 在 JavaMainClass 类里找到名为 "main" 的办法,签名为 "([Ljava/lang/String;)V",修饰符是 public 的静态方法
-> (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs); // 通过 JNI 调用 JavaMainClass.main()办法
以上步骤都还在 java launcher 的管制下;当控制权转移到 JavaMainClass.main()办法之后就没 java launcher 什么事了,等 JavaMainClass.main()办法返回之后 java launcher 才接手过去清理和敞开 JVM。
上面看一下调用 Java 主类 main()办法时会通过的次要办法及执行的次要逻辑,如下:
// 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()办法的真正入口"
前面 3 个步骤是在编译执行的模式下,不过后续咱们从解释执行开始钻研,所以须要为虚拟机配置 -Xint 选项,有了这个选项后,Java 主类的 main()办法就会解释执行了。
在调用 Java 主类 main()办法的过程中,咱们看到了虚拟机是通过 JavaCalls::call()函数来间接调用 main()办法的,下一篇咱们钻研一下具体的调用逻辑。
第 2 篇 -Java 虚拟机这样来调用 Java 主类的 main()办法
在前一篇 第 1 篇 - 对于 Java 虚拟机 HotSpot,开篇说的简略些 中介绍了 call_static()、call_virtual()等函数的作用,这些函数会调用 JavaCalls::call()函数。咱们看 Java 类中 main()办法的调用,调用栈如下:
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
这是 Linux 上的调用栈,通过 JavaCalls::call_helper()函数来执行 main()办法。栈的起始函数为 clone(),这个函数会为每个过程(Linux 过程对应着 Java 线程)创立独自的栈空间,这个栈空间如下图所示。
在 Linux 操作系统上,栈的地址向低地址延长,所以未应用的栈空间在已应用的栈空间之下。图中的每个蓝色小格示意对应办法的栈帧,而栈就是由一个一个的栈帧组成。native 办法的栈帧、Java 解释栈帧和 Java 编译栈帧都会在黄色区域中调配,所以说他们寄生在宿主栈中,这些不同的栈帧都严密的挨在一起,所以并不会产生什么空间碎片这类的问题,而且这样的布局十分有利于进行栈的遍历。下面给出的调用栈就是通过遍历一个一个栈帧失去的,遍历过程也是栈开展的过程。后续对于异样的解决、运行 jstack 打印线程堆栈、GC 查找根援用等都会对栈进行开展操作,所以栈开展是前面必须要介绍的。
上面咱们持续看 JavaCalls::call_helper()函数,这个函数中有个十分重要的调用,如下:
// 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());
}
}
}
调用 StubRoutines::call_stub()函数返回一个函数指针,而后通过函数指针来调用函数指针指向的函数。通过函数指针调用和通过函数名调用的形式一样,这里咱们须要分明的是,调用的指标函数依然是 C /C++ 函数,所以由 C /C++ 函数调用另外一个 C /C++ 函数时,要恪守调用约定。这个调用约定会规定怎么给被调用函数(Callee)传递参数,以及被调用函数的返回值将存储在什么中央。
上面咱们就来简略说说 Linux X86 架构下的 C /C++ 函数调用约定,在这个约定下,以下寄存器用于传递参数:
第 1 个参数:rdi c_rarg0
第 2 个参数:rsi c_rarg1
第 3 个参数:rdx c_rarg2
第 4 个参数:rcx c_rarg3
第 5 个参数:r8 c_rarg4
第 6 个参数:r9 c_rarg5
在函数调用时,6 个及小于 6 个用如下寄存器来传递,在 HotSpot 中通过更易了解的别名 c_rarg* 来应用对应的寄存器。如果参数超过六个,那么程序将会用调用栈来传递那些额定的参数。
数一下咱们通过函数指针调用时传递了几个参数?8 个,那么前面的 2 个就须要通过调用函数(Caller)的栈来传递,这两个参数就是 args->size_of_parameters()和 CHECK(这是个宏,扩大后就是传递线程对象)。
所以咱们的调用栈在调用函数指针指向的函数时,变为了如下状态:
左边是具体的 call_helper()栈帧中的内容,咱们把 thread 和 parameter size 压入了调用栈中,其实在调指标函数的过程还会开拓新的栈帧并在 parameter size 后压入返回地址和调用栈的栈底,下一篇咱们再具体介绍。先来介绍下 JavaCalls::call_helper()函数的实现,咱们分 3 局部顺次介绍。
1、查看指标办法是否 ” 首次执行前就必须被编译”,是的话调用 JIT 编译器去编译指标办法;
代码实现如下:
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);
}
...
}
对于 main()办法来说,如果配置了 -Xint 选项,则是以解释模式执行的,所以并不会走下面的 compile_method()函数的逻辑。后续咱们要钻研编译执行时,能够强制要求进行编译执行,而后查看执行过程。
2、获取指标办法的解释模式入口 from_interpreted_entry,也就是 entry_point 的值。获取的 entry_point 就是为 Java 办法调用筹备栈桢,并把代码调用指针指向 method 的第一个字节码的内存地址。entry_point 相当于是 method 的封装,不同的 method 类型有不同的 entry_point。
接着看 call_helper()函数的代码实现,如下:
address entry_point = method->from_interpreted_entry();
调用 method 的 from_interpreted_entry()函数获取 Method 实例中_from_interpreted_entry 属性的值,这个值到底在哪里设置的呢?咱们前面会具体介绍。
3、调用 call_stub()函数,须要传递 8 个参数。这个代码在后面给出过,这里不再给出。上面咱们具体介绍一下这几个参数,如下:
(1)link 此变量的类型为 JavaCallWrapper,这个变量对于栈开展过程十分重要,前面会具体介绍;
(2)result_val_address 函数返回值地址;
(3)result_type 函数返回类型;
(4)method() 以后要执行的办法。通过此参数能够获取到 Java 办法所有的元数据信息,包含最重要的字节码信息,这样就能够依据字节码信息解释执行这个办法了;
(5)entry_point HotSpot 每次在调用 Java 函数时,必然会调用 CallStub 函数指针,这个函数指针的值取自_call_stub_entry,HotSpot 通过_call_stub_entry 指向被调用函数地址。在调用函数之前,必须要先通过 entry_point,HotSpot 理论是通过 entry_point 从 method()对象上拿到 Java 办法对应的第 1 个字节码命令,这也是整个函数的调用入口;
(6)args->parameters() 形容 Java 函数的入参信息;
(7)args->size_of_parameters() 参数须要占用的,以字为单位的内存大小
(8)CHECK 以后线程对象。
这里最重要的就是 entry_point 了,这也是下一篇要介绍的内容。
第 3 篇 -CallStub 新栈帧的创立
在前一篇文章 第 2 篇 -JVM 虚拟机这样来调用 Java 主类的 main()办法 中咱们介绍了在 call_helper()函数中通过函数指针的形式调用了一个函数,如下:
StubRoutines::call_stub()((address)&link,
result_val_address,
result_type,
method(),
entry_point,
args->parameters(),
args->size_of_parameters(),
CHECK
);
其中调用 StubRoutines::call_stub()函数会返回一个函数指针,查分明这个函数指针指向的函数的实现是咱们这一篇的重点。调用的 call_stub()函数的实现如下:
源代码地位:openjdk/hotspot/src/share/vm/runtime/stubRoutines.hpp
static CallStub call_stub() {return CAST_TO_FN_PTR(CallStub, _call_stub_entry);
}
call_stub()函数返回一个函数指针,指向依赖于操作系统和 cpu 架构的特定的办法,起因很简略,要执行 native 代码,得看看是什么 cpu 架构以便确定寄存器,看看什么 os 以便确定 ABI。
其中 CAST_TO_FN_PTR 是宏,具体定义如下:
源代码地位:/src/share/vm/runtime/utilities/globalDefinitions.hpp
#define CAST_TO_FN_PTR(func_type, value) ((func_type)(castable_address(value)))
对 call_stub()函数进行宏替换和开展后会变为如下的模式:
static CallStub call_stub(){return (CallStub)(castable_address(_call_stub_entry) );
}
CallStub 的定义如下:
源代码地位: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
);
如上定义了一种函数指针类型,指向的函数申明了 8 个形式参数。
在 call_stub()函数中调用的 castable_address()函数定义在 globalDefinitions.hpp 文件中,具体实现如下:
inline address_word castable_address(address x) {return address_word(x) ;
}
address_word 是肯定自定义的类型,在 globalDefinitions.hpp 文件中的定义如下:
typedef uintptr_t address_word;
其中 uintptr_t 也是一种自定义的类型,在 Linux 内核的操作系统下应用 globalDefinitions_gcc.hpp 文件中的定义,具体定义如下:
typedef unsigned int uintptr_t;
这样 call_stub()函数其实等同于如下的实现模式:
static CallStub call_stub(){return (CallStub)(unsigned int(_call_stub_entry) );
}
将_call_stub_entry 强制转换为 unsigned int 类型,而后以强制转换为 CallStub 类型。CallStub 是一个函数指针,所以_call_stub_entry 应该也是一个函数指针,而不应该是一个一般的无符号整数。
在 call_stub()函数中,_call_stub_entry 的定义如下:
address StubRoutines::_call_stub_entry = NULL;
_call_stub_entry 的初始化在在 openjdk/hotspot/src/cpu/x86/vm/stubGenerator_x86_64.cpp 文件下的 generate_initial()函数,调用链如下:
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
其中的 StubGenerator 类定义在 openjdk/hotspot/src/cpu/x86/vm 目录下的 stubGenerator_x86_64.cpp 文件中,这个文件中的 generate_initial()办法会初始化 call_stub_entry 变量,如下:
StubRoutines::_call_stub_entry = generate_call_stub(StubRoutines::_call_stub_return_address);
当初咱们终于找到了函数指针指向的函数的实现逻辑,这个逻辑是通过调用 generate_call_stub()函数来实现的。
不过通过查看后咱们发现这个函数指针指向的并不是一个 C ++ 函数,而是一个机器指令片段,咱们能够将其看为 C ++ 函数通过 C ++ 编译器编译后生成的指令片段即可。在 generate_call_stub()函数中有如下调用语句:
__ enter();
__ subptr(rsp, -rsp_after_call_off * wordSize);
这两段代码间接生成机器指令,不过为了查看机器指令,咱们借助了 HSDB 工具将其反编译为可读性更强的汇编指令。如下:
push %rbp
mov %rsp,%rbp
sub $0x60,%rsp
这 3 条汇编是十分典型的开拓新栈帧的指令。之前咱们介绍过在通过函数指针进行调用之前的栈状态,如下:
那么通过运行如上 3 条汇编后这个栈状态就变为了如下的状态:
咱们须要关注的就是 old %rbp 和 old %rsp 在没有运行开拓新栈帧(CallStub()栈帧)时的指向,以及开拓新栈帧(CallStub()栈帧)时的 new %rbp 和 new %rsp 的指向。另外还要留神 saved rbp 保留的就是 old %rbp,这个值对于栈开展十分重要,因为能通过它一直向上遍历,最终能找到所有的栈帧。
上面接着看 generate_call_stub()函数的实现,如下:
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);
}
// ... 省略了接下来的操作
}
其中开拓新栈帧的逻辑咱们曾经介绍过,上面就是将 call_helper()传递的 6 个在寄存器中的参数存储到 CallStub()栈帧中了,除了存储这几个参数外,还须要存储其它寄存器中的值,因为函数接下来要做的操作是为 Java 办法筹备参数并调用 Java 办法,咱们并不知道 Java 办法会不会毁坏这些寄存器中的值,所以要保留下来,等调用实现后进行复原。
生成的汇编代码如下:
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
加载实现这些参数后如下图所示。
下一篇咱们持续介绍下 generate_call_stub()函数中其余的实现。
第 4 篇 -JVM 终于开始调用 Java 主类的 main()办法啦
在前一篇 第 3 篇 -CallStub 新栈帧的创立 中咱们介绍了 generate_call_stub()函数的局部实现,实现了向 CallStub 栈帧中压入参数的操作,此时的状态如下图所示。
持续看 generate_call_stub()函数的实现,接来下会加载线程寄存器,代码如下:
__ movptr(r15_thread, thread);
__ reinit_heapbase();
生成的汇编代码如下:
mov 0x18(%rbp),%r15
mov 0x1764212b(%rip),%r12 # 0x00007fdf5c6428a8
对照着下面的栈帧可看一下 0x18(%rbp)这个地位存储的是 thread,将这个参数存储到 %r15 寄存器中。
如果在调用函数时有参数的话须要传递参数,代码如下:
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);
这里是个循环,用于传递参数,相当于如下代码:
while(%esi){
rax = *arg
push_arg(rax)
arg++; // ptr++
%esi--; // counter--
}
生成的汇编代码如下:
// 将栈中 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
因为要调用 Java 办法,所以会为 Java 办法压入理论的参数,也就是压入 parameter size 个从 parameters 开始取的参数。压入参数后的栈如下图所示。
当把须要调用 Java 办法的参数准备就绪后,接下来就会调用 Java 办法。这里须要重点提醒一下 Java 解释执行时的办法调用约定,不像 C /C++ 在 x86 下的调用约定一样,不须要通过寄存器来传递参数,而是通过栈来传递参数的,说的更直白一些,是通过局部变量表来传递参数的,所以上图 CallStub()函数栈帧中的 argument word1 … argument word n 其实是被调用的 Java 办法局部变量表的一部分。
上面接着看调用 Java 办法的代码,如下:
// 调用 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);
生成的汇编代码如下:
// 将 Method* 送到 %rbx 中
mov -0x18(%rbp),%rbx
// 将 entry_point 送到 %rsi 中
mov -0x10(%rbp),%rsi
// 将调用者的栈顶指针保留到 %r13 中
mov %rsp,%r13
// 调用 Java 办法
callq *%rsi
留神调用 callq 指令后,会将 callq 指令的下一条指令的地址压栈,再跳转到第 1 操作数指定的地址,也就是 *%rsi 示意的地址。压入下一条指令的地址是为了让函数能通过跳转到栈上的地址从子函数返回。
callq 指令调用的是 entry_point。entry_point 在前面会具体介绍。
第 5 篇 - 调用 Java 办法后弹出栈帧及解决返回后果
在前一篇 第 4 篇 -JVM 终于开始调用 Java 主类的 main()办法啦 介绍了通过 callq 调用 entry point,不过咱们并没有看完 generate_call_stub()函数的实现。接下来在 generate_call_stub()函数中会解决调用 Java 办法后的返回值,同时还须要执行退栈操作,也就是将栈复原到调用 Java 办法之前的状态。调用之前是什么状态呢?在 第 2 篇 -JVM 虚拟机这样来调用 Java 主类的 main()办法 中介绍过,这个状态如下图所示。
generate_call_stub()函数接下来的代码实现如下:
// 保留办法调用后果依赖于后果类型,只有不是 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 指令将地址加载到寄存器中
这里咱们要关注 result 和 result_type,result 在调用 call_helper()函数时就会传递,也就是会批示 call_helper()函数将调用 Java 办法后的返回值存储在哪里。对于类型为 JavaValue 的 result 来说,其实在调用之前就曾经设置了返回类型,所以如上的 result_type 变量只须要从 JavaValue 中获取后果类型即可。例如,调用 Java 主类的 main()办法时,在 jni_CallStaticVoidMethod()函数和 jni_invoke_static()函数中会设置返回类型为 T_VOID,也就是 main()办法返回 void。
生成的汇编代码如下:
// 栈中的 -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
为了让大家看清楚,我贴一下在调用 Java 办法之前的栈帧状态,如下:
由图可看到 -0x60(%rbp)地址指向的地位,恰好不包含调用 Java 办法时压入的理论参数 argument word 1 … argument word n。所以当初 rbp 和 rsp 就是图中指向的地位了。
接下来复原之前保留的 caller-save 寄存器,这也是调用约定的一部分,如下:
__ movptr(r15, r15_save);
__ movptr(r14, r14_save);
__ movptr(r13, r13_save);
__ movptr(r12, r12_save);
__ movptr(rbx, rbx_save);
__ ldmxcsr(mxcsr_save);
生成的汇编代码如下:
mov -0x58(%rbp),%r15
mov -0x50(%rbp),%r14
mov -0x48(%rbp),%r13
mov -0x40(%rbp),%r12
mov -0x38(%rbp),%rbx
ldmxcsr -0x60(%rbp)
在弹出了为调用 Java 办法保留的理论参数及复原 caller-save 寄存器后,继续执行退栈操作,实现如下:
// restore rsp
__ addptr(rsp, -rsp_after_call_off * wordSize);
// return
__ pop(rbp);
__ ret(0);
生成的汇编代码如下:
// %rsp 加上 0x60,也就是执行退栈操作,也就相
// 当于弹出了 callee_save 寄存器和压栈的那 6 个参数
add $0x60,%rsp
pop %rbp
// 办法返回,指令中的 q 示意 64 位操作数,就是指
// 的栈中存储的 return address 是 64 位的
retq
记得在之前 第 3 篇 -CallStub 新栈帧的创立时,通过如下的汇编实现了新栈帧的创立:
push %rbp
mov %rsp,%rbp
sub $0x60,%rsp
当初要退出这个栈帧时要在 %rsp 指向的地址加上 $0x60,同时复原 %rbp 的指向。而后就是跳转到 return address 指向的指令继续执行了。
为了不便大家查看,我再次给出了之前应用到的图片,这个图是退栈之前的图片:
退栈之后如下图所示。
至于 paramter size 与 thread 则由 JavaCalls::call_hlper()函数负责开释,这是 C /C++ 调用约定的一部分。所以如果不看这 2 个参数,咱们曾经齐全回到了本篇给出的第一张图示意的栈的样子。
下面这些图片大家应该不生疏才对,咱们在一步步创立栈帧时都给出过,当初怎么创立的就会怎么退出。
之前介绍过,当 Java 办法返回 int 类型时(如果返回 char、boolean、short 等类型时对立转换为 int 类型),依据 Java 办法调用约定,这个返回的 int 值会存储到 %rax 中;如果返回对象,那么 %rax 中存储的就是这个对象的地址,那前面到底怎么辨别是地址还是 int 值呢?答案是通过返回类型辨别即可;如果返回非 int,非对象类型的值呢?咱们持续看 generate_call_stub()函数的实现逻辑:
// 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);
对应的汇编代码如下:
// -- is_long --
mov %rax,(%rdi)
jmp 0x00007fdf450007d4
// -- is_float --
vmovss %xmm0,(%rdi)
jmp 0x00007fdf450007d4
// -- is_double --
vmovsd %xmm0,(%rdi)
jmp 0x00007fdf450007d4
当返回 long 类型时也存储到 %rax 中,因为 Java 的 long 类型是 64 位,咱们剖析的代码也是 x86 下 64 位的实现,所以 %rax 寄存器也是 64 位,可能包容 64 位数;当返回为 float 或 double 时,存储到 %xmm0 中。
统合这一篇和前几篇文章,咱们应该学习到 C /C++ 的调用约定以及 Java 办法在解释执行下的调用约定(包含如何传递参数,如何接管返回值等),如果大家不明确,多读几遍文章就会有一个清晰的意识。
第 6 篇 -Java 办法新栈帧的创立
在 第 2 篇 -JVM 虚拟机这样来调用 Java 主类的 main()办法 介绍 JavaCalls::call_helper()函数的实现时提到过如下一句代码:
address entry_point = method->from_interpreted_entry();
这个参数会做为实参传递给 StubRoutines::call_stub()函数指针指向的“函数”,而后在 第 4 篇 -JVM 终于开始调用 Java 主类的 main()办法啦 介绍到通过 callq 指令调用 entry_point,那么这个 entry_point 到底是什么呢?这一篇咱们将具体介绍。
首先看 from_interpreted_entry()函数实现,如下:
源代码地位:/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 只是 Method 类中定义的一个属性,如上办法间接返回了这个属性的值。那么这个属性是何时赋值的?其实是在办法连贯(也就是在类的生命周期中的类连贯阶段会进行办法连贯)时会设置。办法连贯时会调用如下办法:
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);
// ...
}
首先调用 Interpreter::entry_for_method()函数依据特定办法类型获取到办法的入口,失去入口 entry 后会调用 set_interpreter_entry()函数将值保留到对应属性上。set_interpreter_entry()函数的实现非常简单,如下:
void set_interpreter_entry(address entry) {
_i2i_entry = entry;
_from_interpreted_entry = entry;
}
能够看到为_from_interpreted_entry 属性设置了 entry 值。
上面看一下 entry_for_method()函数的实现,如下:
static address entry_for_method(methodHandle m) {return entry_for_kind(method_kind(m));
}
首先通过 method_kind()函数拿到办法对应的类型,而后调用 entry_for_kind()函数依据办法类型获取办法对应的入口 entry_point。调用的 entry_for_kind()函数的实现如下:
static address entry_for_kind(MethodKind k){return _entry_table[k];
}
这里间接返回了_entry_table 数组中对应办法类型的 entry_point 地址。
这里波及到 Java 办法的类型 MethodKind,因为要通过 entry_point 进入 Java 世界,执行 Java 办法相干的逻辑,所以 entry_point 中肯定会为对应的 Java 办法建设新的栈帧,然而不同办法的栈帧其实是有差异的,如 Java 一般办法、Java 同步办法、有 native 关键字的 Java 办法等,所以就把所有的办法进行了归类,不同类型获取到不同的 entry_point 入口。到底有哪些类型,咱们能够看一下 MethodKind 这个枚举类中定义出的枚举常量:
enum MethodKind {
zerolocals, // 一般的办法
zerolocals_synchronized, // 一般的同步办法
native, // native 办法
native_synchronized, // native 同步办法
...
}
当然还有其它一些类型,不过最次要的就是如上枚举类中定义出的 4 种类型办法。
为了能尽快找到某个 Java 办法对应的 entry_point 入口,把这种对应关系保留到了_entry_table 中,所以 entry_for_kind()函数能力疾速的获取到办法对应的 entry_point 入口。给数组中元素赋值专门有个办法:
void AbstractInterpreter::set_entry_for_kind(AbstractInterpreter::MethodKind kind, address entry) {_entry_table[kind] = entry;
}
那么何时会调用 set_entry_for_kind()函数呢,答案就在 TemplateInterpreterGenerator::generate_all()函数中,generate_all()函数会调用 generate_method_entry()函数生成每种 Java 办法的 entry_point,每生成一个对应办法类型的 entry_point 就保留到_entry_table 中。
上面具体介绍一下 generate_all()函数的实现逻辑,在 HotSpot 启动时就会调用这个函数生成各种 Java 办法的 entry_point。调用栈如下:
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
调用的 generate_all()函数将生成一系列 HotSpot 运行过程中所执行的一些公共代码的入口和所有字节码的 InterpreterCodelet,一些十分重要的入口实现逻辑会在前面具体介绍,这里只看一般的、没有 native 关键字润饰的 Java 办法生成入口的逻辑。generate_all()函数中有如下实现:
#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)
其中 method_entry 是一个宏,扩大后如上的 method_entry(zerolocals)语句变为如下的模式:
Interpreter::_entry_table[Interpreter::zerolocals] = generate_method_entry(Interpreter::zerolocals);
_entry_table 变量定义在 AbstractInterpreter 类中,如下:
static address _entry_table[number_of_method_entries];
number_of_method_entries 示意办法类型的总数,应用办法类型做为数组下标就能够获取对应的办法入口。调用 generate_method_entry()函数为各种类型的办法生成对应的办法入口。generate_method_entry()函数的实现如下:
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 示意失常的 Java 办法调用,包含 Java 程序的 main()办法,对于 zerolocals 来说,会调用 ig_this->generate_normal_entry()函数生成入口。generate_normal_entry()函数会为执行的办法生成堆栈,而堆栈由局部变量表(用来存储传入的参数和被调用办法的局部变量)、Java 办法栈帧数据和操作数栈这三大部分组成,所以 entry_point 例程(其实就是一段机器指令片段,英文名为 stub)会创立这 3 局部来辅助 Java 办法的执行。
咱们还是回到开篇介绍的知识点,通过 callq 指令调用 entry_point 例程。此时的栈帧状态在 第 4 篇 -JVM 终于开始调用 Java 主类的 main()办法啦 中介绍过,为了大家浏览的不便,这里再次给出:
留神,在执行 callq 指令时,会将函数的返回地址存储到栈顶,所以上图中会压入 return address 一项。
CallStub()函数在通过 callq 指令调用 generate_normal_entry()函数生成的 entry_point 时,有几个寄存器中存储着重要的值,如下:
rbx -> Method*
r13 -> sender sp
rsi -> entry point
上面就是剖析 generate_normal_entry()函数的实现逻辑了,这是调用 Java 办法的最重要的局部。函数的重要实现逻辑如下:
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;
}
这个函数的实现看起来比拟多,但其实逻辑实现比较简单,就是依据被调用办法的理论状况创立出对应的局部变量表,而后就是 2 个十分重要的函数 generate_fixed_frame()和 dispatch_next()函数了,这 2 个函数咱们前面再具体介绍。
在调用 generate_fixed_frame()函数之前,栈的状态变为了下图所示的状态。
与前一个图比照一下,能够看到多了一些 local variable 1 … local variable n 等 slot,这些 slot 与 argument word 1 … argument word n 独特形成了被调用的 Java 办法的局部变量表,也就是图中紫色的局部。其实 local variable 1 … local variable n 等 slot 属于被调用的 Java 办法栈帧的一部分,而 argument word 1 … argument word n 却属于 CallStub()函数栈帧的一部分,这 2 局部独特形成局部变量表,专业术语叫栈帧重叠。
另外还能看进去,%r14 指向了局部变量表的第 1 个参数,而 CallStub()函数的 return address 被保留到了 %rax 中,另外 %rbx 中仍然存储着 Method*。这些寄存器中保留的值将在调用 generate_fixed_frame()函数时用到,所以咱们须要在这里强调一下。
第 7 篇 - 为 Java 办法创立栈帧
在 第 6 篇 -Java 办法新栈帧的创立 介绍过局部变量表的创立,创立实现后的栈帧状态如下图所示。
各个寄存器的状态如下所示。
// %rax 寄存器中存储的是返回地址
rax: return address
// 要执行的 Java 办法的指针
rbx: Method*
// 本地变量表指针
r14: pointer to locals
// 调用者的栈顶
r13: sender sp
留神 rax 中保留的返回地址,因为在 generate_call_stub()函数中通过__ call(c_rarg1) 语句调用了由 generate_normal_entry()函数生成的 entry_point,所以当 entry_point 执行实现后,还会返回到 generate_call_stub()函数中继续执行__ call(c_rarg1) 语句上面的代码,也就是
第 5 篇 - 调用 Java 办法后弹出栈帧及解决返回后果 波及到的那些代码。
调用的 generate_fixed_frame()函数的实现如下:
源代码地位: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);
}
对于一般的 Java 办法来说,生成的汇编代码如下:
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 0x00007fffed01b27d
add $0x90,%rdx
push %rdx
mov 0x10(%rbx),%rdx
mov 0x8(%rdx),%rdx
mov 0x18(%rdx),%rdx
push %rdx
push %r14
push %r13
pushq $0x0
mov %rsp,(%rsp)
汇编比较简单,这里不再多说。执行完如上的汇编后生成的栈帧状态如下图所示。
调用完 generate_fixed_frame()函数后一些寄存器中保留的值如下:
rbx:Method*
ecx:invocation counter
r13:bcp(byte code pointer)
rdx:ConstantPool* 常量池的地址
r14:本地变量表第 1 个参数的地址
执行完 generate_fixed_frame()函数后会持续返回执行 InterpreterGenerator::generate_normal_entry()函数,如果是为同步办法生成机器码,那么还须要调用 lock_method()函数,这个函数会扭转以后栈帧的状态,增加同步所须要的一些信息,在前面介绍锁的实现时会具体介绍。
InterpreterGenerator::generate_normal_entry()函数最终会返回生成机器码的入口执行地址,而后通过变量_entry_table 数组来保留,这样就能够应用办法类型做为数组下标获取对应的办法入口了。
第 8 篇 -dispatch_next()函数分派字节码
在 generate_normal_entry()函数中会调用 generate_fixed_frame()函数为 Java 办法的执行生成对应的栈帧,接下来还会调用 dispatch_next()函数执行 Java 办法的字节码。generate_normal_entry()函数调用的 dispatch_next()函数之前一些寄存器中保留的值如下:
rbx:Method*
ecx:invocation counter
r13:bcp(byte code pointer)
rdx:ConstantPool* 常量池的地址
r14:本地变量表第 1 个参数的地址
dispatch_next()函数的实现如下:
// 从 generate_fixed_frame()函数生成 Java 办法调用栈帧的时候,// 如果以后是第一次调用,那么 r13 指向的是字节码的首地址,// 即第一个字节码,此时的 step 参数为 0
void InterpreterMacroAssembler::dispatch_next(TosState state, int step) {load_unsigned_byte(rbx, Address(r13, step));
// 在以后字节码的地位,指针向前挪动 step 宽度,// 获取地址上的值,这个值是 Opcode(范畴 1~202),存储到 rbx
// step 的值由字节码指令和它的操作数独特决定
// 自增 r13 供下一次字节码分派应用
increment(r13, step);
// 返回以后栈顶状态的所有字节码入口点
dispatch_base(state, Interpreter::dispatch_table(state));
}
r13 指向字节码的首地址,当第 1 次调用时,参数 step 的值为 0,那么 load_unsigned_byte()函数从 r13 指向的内存中取一个字节的值,取出来的是字节码指令的操作码。减少 r13 的步长,这样下次执行时就会取出来下一个字节码指令的操作码。
调用的 dispatch_table()函数的实现如下:
static address* dispatch_table(TosState state) {return _active_table.table_for(state);
}
在_active_table 中获取对应栈顶缓存状态的入口地址,_active_table 变量定义在 TemplateInterpreter 类中,如下:
static DispatchTable _active_table;
DispatchTable 类及 table_for()等函数的定义如下:
DispatchTable TemplateInterpreter::_active_table;
class DispatchTable VALUE_OBJ_CLASS_SPEC {
public:
enum {length = 1 << BitsPerByte}; // BitsPerByte 的值为 8
private:
// number_of_states=9,length=256
// _table 是字节码分发表
address _table[number_of_states][length];
public:
// ...
address* table_for(TosState state){return _table[state];
}
address* table_for(){return table_for((TosState)0);
}
// ...
};
address 为 u_char* 类型的别名。_table 是一个二维数组的表,维度为栈顶状态(共有 9 种)和字节码(最多有 256 个),存储的是每个栈顶状态对应的字节码的入口点。这里因为还没有介绍栈顶缓存,所以了解起来并不容易,不过前面会具体介绍栈顶缓存和字节码分发表的相干内容,等介绍完了再看这部分逻辑就比拟容易了解了。
InterpreterMacroAssembler::dispatch_next()函数中调用的 dispatch_base()函数的实现如下:
void InterpreterMacroAssembler::dispatch_base(
TosState state, // 示意栈顶缓存状态
address* table,
bool verifyoop
) {
// ...
// 获取以后栈顶状态字节码转发表的地址,保留到 rscratch1
lea(rscratch1, ExternalAddress((address)table));
// 跳转到字节码对应的入口执行机器码指令
// address = rscratch1 + rbx * 8
jmp(Address(rscratch1, rbx, Address::times_8));
}
比方取一个字节大小的指令(如 iconst_0、aload_0 等都是一个字节大小的指令),那么 InterpreterMacroAssembler::dispatch_next()函数生成的汇编代码如下:
// 在 generate_fixed_frame()函数中
// 曾经让 %r13 存储了 bcp
// %ebx 中存储的是字节码的 Opcode,也就是操作码
movzbl 0x0(%r13),%ebx
// $0x7ffff73ba4a0 这个地址指向的
// 是对应 state 状态下的一维数组,长度为 256
movabs $0x7ffff73ba4a0,%r10
// 留神 %r10 中存储的是常量,依据计算公式
// %r10+%rbx* 8 来获取指向存储入口地址的地址,// 通过 *(%r10+%rbx*8)获取到入口地址,// 而后跳转到入口地址执行
jmpq *(%r10,%rbx,8)
%r10 指向的是对应栈顶缓存状态 state 下的一维数组,长度为 256,其中存储的值为 opcode,如下图所示。
上面的函数显示了对每个字节码的每个栈顶状态都设置入口地址。
void DispatchTable::set_entry(int i, EntryPoint& entry) {assert(0 <= i && i < length, "index out of bounds");
assert(number_of_states == 9, "check the code below");
_table[btos][i] = entry.entry(btos);
_table[ctos][i] = entry.entry(ctos);
_table[stos][i] = entry.entry(stos);
_table[atos][i] = entry.entry(atos);
_table[itos][i] = entry.entry(itos);
_table[ltos][i] = entry.entry(ltos);
_table[ftos][i] = entry.entry(ftos);
_table[dtos][i] = entry.entry(dtos);
_table[vtos][i] = entry.entry(vtos);
}
其中的参数 i 就是 opcode,各个字节码及对应的 opcode 可参考 https://docs.oracle.com/javas…。
所以_table 表如下图所示。
_table 的一维为栈顶缓存状态,二维为 Opcode,通过这 2 个维度可能找到一段机器指令,这就是依据以后的栈顶缓存状态定位到的字节码须要执行的机器指令片段。
调用 dispatch_next()函数执行 Java 办法的字节码,其实就是依据字节码找到对应的机器指令片段的入口地址来执行,这段机器码就是依据对应的字节码语义翻译过去的,这些都会在前面具体介绍。
公众号【深刻分析 Java 虚拟机 HotSpot】曾经更新虚拟机源代码分析相干文章到 60+,欢送关注,如果有任何问题,可加作者微信 mazhimazh,拉你入虚拟机群交换。