0. 简介
众所周知, hotspot 默认使用解释 + 编译混合 (-Xmixed) 的方式执行代码. 它首先使用模板解释器对字节码进行解释, 当发现一段代码是热点的时候, 就使用 C1/C2 JIT 进行优化编译再执行, 这也它的名字 "热点"(hotspot)的由来.
解释器的代码位于 hotspot/share/interpreter, 它的总体架构如下:
1. 解释器的两种实现
首先 hotspot 有一个 C++ 字节码解释器, 还有一个模板解释器 , 默认使用的是模板解释器的实现. 这两个有什么区别呢? 举个例子, Java 字节码有 istore_0,iadd, 如果是 C++ 字节码解释器(图右部分), 那么它的工作流程就是这种:
- void cppInterpreter::work(){
- for(int i=0;i<bytecode.length();i++){
- switch(bytecode[i]){
- case ISTORE_0:
- int value = operandStack.pop();
- localVar[0] = value;
- break;
- case IADD:
- int v1 = operandStack.pop();
- int v2 = operandStack.pop();
- int res = v1+v2;
- operandStack.push(res);
- break;
- ....
- }
- }
- }
它使用 C++ 语言模拟字节码的执行, iadd 是两个数相加, 字节码解释器从栈上 pop 两个数据然后求和, 再 push 到栈上.
如果是模板解释器就完全不一样了. 模板解释器是一堆本地码的例程(routines), 它会在虚拟机创建的时候初始化好, 也就是说, 模板解释器在初始化的时候会申请一片内存并设置为可读可写可执行, 然后向那片内存写入本地码. 在解释执行的时候遇到 iadd, 就执行那片内存里面的二进制代码, 类似这样:
- #include <Windows.h>
- #include <cstdint>
- #include <cstring>
- #define BACK_FILL (0)
- int main() {
- char* native = (char*)VirtualAlloc(NULL, 1024, MEM_COMMIT | MEM_RESERVE,
- PAGE_EXECUTE_READWRITE);
- // 输出 hello world 的机器码, 仅限于 x86+Windows
- char code[] = {0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77,
- 0x6f, 0x72, 0x6c, 0x64, 0x00, 0xff, 0xf5,
- 0x89, 0xe5, 0x68, BACK_FILL, BACK_FILL, BACK_FILL, BACK_FILL,
- 0xff, 0x95, 0x08, 0x00, 0x00, 0x00, 0x81,
- 0xc4, 0x04, 0x00, 0x00, 0x00, 0x8f, 0xc5,
- 0xc3, 0x00};
- memcpy(native, code, sizeof(code) / sizeof(char));
- *(int32_t*)(native + 17) = (int32_t)native;
- ((void (*)(int (*)(const char*, ...)))(native + 12))(&printf);
- VirtualFree(native, 0, MEM_RELEASE);
- getchar();
- return 0;
- }
这种运行时代码生成的机制可以说是 JIT, 只是通常意义的 JIT 是指对一块代码进行优化再生成本地代码, 同一段代码可能因为分成编译产出不同的本地码, 具有动态性; 而模板解释器是虚拟机在创建的时候 JIT 生成它自身, 它的每个例程比如异常处理部分, 安全点处理部分的本地码都是固定的, 是静态的.
2. 解释器
2.1 抽象解释器
再回到主题, 架构图有一个抽象解释器, 这个抽象解释器描述了解释器的基本骨架, 它的属性如下:
- class AbstractInterpreter{
- StubQueue* _code
- address _slow_signature_handler;
- address _entry_table[n];
- address _cds_entry_table[n];
- ...
- };
所有的解释器 (C++ 字节码解释器, 模板解释器) 都有这些例程和属性, 然后子类的解释器还可以再扩展一些例程.
我们重点关注_code, 它是一个队列,
队列中的 InterpreterCodelet 表示一个小例程, 比如 iconst_1 对应的代码, invokedynamic 对应的代码, 异常处理对应的代码, 方法入口点对应的代码, 这些代码都是一个个 InterpreterCodelet... 整个解释器都是由这些小块代码例程组成的, 每个小块例程完成解释器的部分功能, 以此实现整个解释器.
_entry_table 也是个重要的属性, 这个数组表示方法的例程, 比如普通方法是入口点 1_entry_table[0], 带 synchronized 的方法是入口点 2_entry_table[1], 这些_entry_table[0],_entry_table[1]指向的就是之前_code 队列里面的小块例程, 就像这样:
- _entry_table[0] = _code->get_stub("iconst_1")->get_address();
- _entry_table[1] = _code->get_stub("fconst_1")->get_address();
当然实际的实现远比伪代码复杂.
2.2 模板解释器
前面说道小块例程组合起来实现了解释器, 抽象解释器定义了必要的例程, 具体的解释器在这之上还有自己的特设的例程. 模板解释器就是一个例子, 它继承自抽象解释器, 在那些例程之上还有自己的特设例程:
- // 各种异常处理的例程
- static address _throw_ArrayIndexOutOfBoundsException_entry;
- static address _throw_ArrayStoreException_entry;
- static address _throw_ArithmeticException_entry;
- static address _throw_ClassCastException_entry;
- static address _throw_NullPointerException_entry;
- static address _throw_exception_entry;
- static address _throw_StackOverflowError_entry;
- static address _remove_activation_entry;
- // 从调用中返回的例程
- static EntryPoint _return_entry[number_of_return_entries];
- ...
这样做的好处是可以针对一些特殊例程进行特殊处理, 同时还可以复用代码.
到这里解释器的布局应该是说清楚了, 我们大概知道了: 解释器是一堆本地代码例程构造的, 这些例程会在虚拟机启动的时候写入, 以后解释就只需要进入指定例程即可.
3. 解释器生成器
还有一个问题, 这些例程是谁写入的呢? 找一找架构图, 下半部分都是解释器生成器, 那么它就是答案了.
前面刻意说道解释器布局就是想突出它只是一个骨架, 要得到可运行的解释器还需要解释器生成器填充这个骨架.
解释器生成器本来可以独自完成填充工作, 可能为了解耦, 也可能是为了结构清晰, hotspot 将字节码的例程抽了出来放到了 templateTable(模板表)中, 它辅助模板解释器生成器 (templateInterpreterGenerator) 完成各例程填充.
只有这两个还不能完成任务, 因为组成模板解释器的是本地代码例程, 本地代码例程依赖于操作系统和 CPU, 这部分代码位于 hotspot/CPU/x86 / 中, 所以
- templateInterpreter =
- templateTable +
- templateTable_x86 +
- templateInterpreterGenerator +
- templateInterpreterGenerator_x86 +
- templateInterpreterGenerator_x86_64
来源: https://www.cnblogs.com/kelthuzadx/p/10707504.html