共计 46516 个字符,预计需要花费 117 分钟才能阅读完成。
本文由 HeapDump 性能社区首席讲师鸠摩(马智)受权整顿公布
第 9 篇 - 字节码指令的定义
之前的文章介绍了解释执行下的 Java 栈帧创立以及字节码分派逻辑,然而始终没有讲到虚拟机到底是怎么执行 Java 办法中的字节码的,在介绍字节码的执行之前,须要先晓得字节码指令的定义。在 Bytecodes::initialize()函数中会定义字节码指令的一些属性。这个函数的调用链如下:
init_globals()
bytecodes_init()
Bytecodes::initialize()
在 Bytecodes::initialize()函数中有相似这样的定义:
// bytecode bytecode name format wide f. result tp stk traps
def(_nop , "nop" , "b" , NULL , T_VOID , 0, false);
def(_aconst_null , "aconst_null" , "b" , NULL , T_OBJECT , 1, false);
def(_iconst_m1 , "iconst_m1" , "b" , NULL , T_INT , 1, false);
def(_iconst_0 , "iconst_0" , "b" , NULL , T_INT , 1, false);
def(_iconst_1 , "iconst_1" , "b" , NULL , T_INT , 1, false);
// ...
当初 Java 虚拟机标准定义的 202 个字节码指令都会向上图那样,调用 def()函数进行定义,咱们须要重点关注调用 def()函数时传递的参数 bytecode name、format 等。上面一个一个解释,如下:
- bytecode name 就是字节码名称;
- wide 示意字节码后面是否能够加 wide,如果能够,则值为 ”wbii”;
- result tp 示意指令执行后的后果类型,如为 T_ILLEGAL 时,示意只参考以后字节码无奈决定执行后果的类型,如_invokevirtual 办法调用指令,后果类型应该为办法返回类型,然而此时只参考这个调用办法的字节码指令是无奈决定的;
- stk 示意对表达式栈深度的影响,如_nop 指令不执行任何操作,所以对表达式栈的深度无影响,stk 的值为 0;当用_iconst_0 向栈中压入 0 时,栈的深度减少 1,所以 stk 的值为 1。当为_lconst_0 时,栈的深度会减少 2;当为_lstore_0 时,栈的深度会缩小 2;
- traps 示意 can_trap,这个比拟重要,在前面会具体介绍。
- format,这个属性能表白 2 个意思,首先能表白字节码的格局,另外还能示意字节码的长度。
上面咱们须要重点介绍一下 format 这个参数。format 示意字节码的格局,当字符串中有一个字符时就是一个字节长度的字节码,当为 2 个字符时就是 2 个字节长度的字节码 …,如_iconst_0 就是一个字节宽度的字节码,_istore 的 format 为 ”bi”,所以是 2 个字节宽度。format 还可能为空字符串,当为空字符串时,示意以后的字节码不是 Java 虚拟机标准中定义的字节码,如为了进步解释执行效率的_fast_agetfield、_fast_bgetfield 等字节码,这些字节码是虚拟机外部定义的。还能表白字节码的格局,其中的字符串中各个字符的含意如下:
b:示意字节码指令是非可变长度的,所以对于 tableswitch、lookupswitch 这种可变长度的指令来说,format 字符串中不会含有 b 字符;
c:操作数为有符号的常量,如 bipush 指令将 byte 带符号扩大为一个 int 类型的值,而后将这个值入栈到操作数栈中;
i:操作数为无符号的本地变量表索引值,如 iload 指令从局部变量表加载一个 int 类型的值到操作数栈中;
j:操作数为常量池缓存的索引,留神常量池缓存索引不同与常量池索引,对于常量池索引,在《深刻分析 Java 虚拟机:源码分析与实例详解》根底卷中具体介绍过,这里不再介绍;
k:操作数为无符号的常量池索引,如 ldc 指令将从运行时常量池中提取数据并压入操作数栈,所以格局为 ”bk”;
o:操作数为分支偏移,如 ifeq 示意整数与零比拟,如果整数为 0,则比拟后果为真,将操作数看为分支偏移量进行跳转,所以格局为”boo“;
_:可间接疏忽
w:可用来扩大局部变量表索引的字节码,这些字节码有 iload、fload 等,所以 wild 的值为 ”wbii”;
调用的 def()函数的实现如下:
void Bytecodes::def(
Code code,
const char* name,
const char* format,
const char* wide_format,
BasicType result_type,
int depth,
bool can_trap,
Code java_code
) {int len = (format != NULL ? (int) strlen(format) : 0);
int wlen = (wide_format != NULL ? (int) strlen(wide_format) : 0);
_name = name;
_result_type = result_type;
_depth = depth;
_lengths = (wlen << 4) | (len & 0xF); // 0xF 的二进制值为 1111
_java_code = java_code;
int bc_flags = 0;
if (can_trap){
// ldc、ldc_w、ldc2_w、_aload_0、iaload、iastore、idiv、ldiv、ireturn 等
// 字节码指令都会含有_bc_can_trap
bc_flags |= _bc_can_trap;
}
if (java_code != code){bc_flags |= _bc_can_rewrite; // 虚拟机外部定义的指令都会有_bc_can_rewrite}
// 在这里对_flags 赋值操作
_flags[(u1)code+0*(1<<BitsPerByte)] = compute_flags(format, bc_flags);
_flags[(u1)code+1*(1<<BitsPerByte)] = compute_flags(wide_format, bc_flags);
}
其中的_name、_result_type 等都是在 Bytecodes 类中定义的动态数组,其下标为 Opcode 值,而存储的值就是 name、result_type 等。这些变量的定义如下:
const char* Bytecodes::_name [Bytecodes::number_of_codes];
BasicType Bytecodes::_result_type [Bytecodes::number_of_codes];
s_char Bytecodes::_depth [Bytecodes::number_of_codes];
u_char Bytecodes::_lengths [Bytecodes::number_of_codes];
Bytecodes::Code Bytecodes::_java_code [Bytecodes::number_of_codes];
u_short Bytecodes::_flags [(1<<BitsPerByte)*2];
Bytecodes::number_of_codes 的值为 234,足够存储所有的字节码指令了(蕴含虚拟机外部扩大的指令)。
回看 Bytecodes::def()函数,通过调用 compute_flags()函数依据传入的 wide_format 和 format 来计算字节码的一些属性,而后存储到高 8 位和低 8 位中。调用的 compute_flags()函数的实现如下:
int Bytecodes::compute_flags(const char* format, int more_flags) {if (format == NULL) {return 0; // not even more_flags}
int flags = more_flags;
const char* fp = format;
switch (*fp) {
case '\0':
flags |= _fmt_not_simple; // but variable
break;
case 'b':
flags |= _fmt_not_variable; // but simple
++fp; // skip 'b'
break;
case 'w':
flags |= _fmt_not_variable | _fmt_not_simple;
++fp; // skip 'w'
guarantee(*fp == 'b', "wide format must start with'wb'");
++fp; // skip 'b'
break;
}
int has_nbo = 0, has_jbo = 0, has_size = 0;
for (;;) {
int this_flag = 0;
char fc = *fp++;
switch (fc) {
case '\0': // end of string
assert(flags == (jchar)flags, "change _format_flags");
return flags;
case '_': continue; // ignore these
case 'j': this_flag = _fmt_has_j; has_jbo = 1; break;
case 'k': this_flag = _fmt_has_k; has_jbo = 1; break;
case 'i': this_flag = _fmt_has_i; has_jbo = 1; break;
case 'c': this_flag = _fmt_has_c; has_jbo = 1; break;
case 'o': this_flag = _fmt_has_o; has_jbo = 1; break;
case 'J': this_flag = _fmt_has_j; has_nbo = 1; break;
...
default: guarantee(false, "bad char in format");
}// 完结 switch
flags |= this_flag;
guarantee(!(has_jbo && has_nbo), "mixed byte orders in format");
if (has_nbo){flags |= _fmt_has_nbo;}
int this_size = 1;
if (*fp == fc) {
// advance beyond run of the same characters
this_size = 2;
while (*++fp == fc){this_size++;}
switch (this_size) {
case 2: flags |= _fmt_has_u2; break; // 如 sipush、ldc_w、ldc2_w、wide iload 等
case 4: flags |= _fmt_has_u4; break; // 如 goto_w 和 invokedynamic 指令
default:
guarantee(false, "bad rep count in format");
}
}
has_size = this_size;
}
}
函数要依据 wide_format 和 format 来计算 flags 的值,通过 flags 中的值可能示意字节码的 b、c、i、j、k、o、w(在之前介绍 format 时介绍过)和字节码操作数的大小(操作数是 2 字节还是 4 字节)。以_fmt 结尾的一些变量在枚举类中曾经定义,如下:
// Flag bits derived from format strings, can_trap, can_rewrite, etc.:
enum Flags {
// semantic flags:
_bc_can_trap = 1<<0, // bytecode execution can trap(卡住) or block
// 虚拟机外部定义的字节码指令都会含有这个标识
_bc_can_rewrite = 1<<1, // bytecode execution has an alternate(代替者) form
// format bits (determined only by the format string):
_fmt_has_c = 1<<2, // constant, such as sipush "bcc"
_fmt_has_j = 1<<3, // constant pool cache index, such as getfield "bjj"
_fmt_has_k = 1<<4, // constant pool index, such as ldc "bk"
_fmt_has_i = 1<<5, // local index, such as iload
_fmt_has_o = 1<<6, // offset, such as ifeq
_fmt_has_nbo = 1<<7, // contains native-order field(s)
_fmt_has_u2 = 1<<8, // contains double-byte field(s)
_fmt_has_u4 = 1<<9, // contains quad-byte field
_fmt_not_variable = 1<<10, // not of variable length (simple or wide) 不可变长度的指令
_fmt_not_simple = 1<<11, // either wide or variable length 或者是可加 wild 的字节码指令,或者是可变长度的指令
_all_fmt_bits = (_fmt_not_simple*2 - _fmt_has_c),
// ...
};
与 format 的对应关系如下:
这样通过组合就可示意出不同的值,枚举类中定义了罕用的组合如下:
_fmt_b = _fmt_not_variable,
_fmt_bc = _fmt_b | _fmt_has_c,
_fmt_bi = _fmt_b | _fmt_has_i,
_fmt_bkk = _fmt_b | _fmt_has_k | _fmt_has_u2,
_fmt_bJJ = _fmt_b | _fmt_has_j | _fmt_has_u2 | _fmt_has_nbo,
_fmt_bo2 = _fmt_b | _fmt_has_o | _fmt_has_u2,
_fmt_bo4 = _fmt_b | _fmt_has_o | _fmt_has_u4
例如字节码为 bipush 时,format 就是 "bc",那么 flags 的值为_fmt_b | _fmt_has_c,ldc 字节码的 format 为 "bk",则 flags 的值为_fmt_b | _fmt_has_k。
第 10 篇 - 初始化模板表
在 第 9 篇 - 字节码指令的定义 咱们介绍了字节码指令并且将字节码指令相干的信息都存储到了相干数组中,只须要通过 Opcode 就可从相干数组中获取对应的信息。
在 init_globals()函数中调用 bytecodes_init()函数初始化好字节码指令后会调用 interpreter_init()函数初始化解释器。函数最终会调用到 TemplateInterpreter::initialize()函数。这个函数的实现如下:
源代码地位:/src/share/vm/interpreter/templateInterpreter.cpp
void TemplateInterpreter::initialize() {if (_code != NULL)
return;
// 形象解释器 AbstractInterpreter 的初始化,// AbstractInterpreter 是基于汇编模型的解释器的独特基类,// 定义了解释器和解释器生成器的形象接口
AbstractInterpreter::initialize();
// 模板表 TemplateTable 的初始化,模板表 TemplateTable 保留了各个字节码的模板
TemplateTable::initialize();
// generate interpreter
{
ResourceMark rm;
int code_size = InterpreterCodeSize;
// CodeCache 的 Stub 队列 StubQueue 的初始化
_code = new StubQueue(new InterpreterCodeletInterface, code_size, NULL,"Interpreter");
// 实例化模板解释器生成器对象 TemplateInterpreterGenerator
InterpreterGenerator g(_code);
}
// 初始化字节分发表
_active_table = _normal_table;
}
这个初始化函数中波及到的初始化逻辑比拟多,而且比较复杂。咱们将初始化分为 4 局部:
(1)形象解释器 AbstractInterpreter 的初始化,AbstractInterpreter 是基于汇编模型的解释器的独特基类,定义了解释器和解释器生成器的形象接口。
(2)模板表 TemplateTable 的初始化,模板表 TemplateTable 保留了各个字节码的模板(指标代码生成函数和参数);
(3)CodeCache 的 Stub 队列 StubQueue 的初始化;
(4)解释器生成器 InterpreterGenerator 的初始化。
其中形象解释器初始化时会波及到一些计数,这些计数次要与编译执行无关,所以这里暂不过多介绍,到前面介绍编译执行时再介绍。
上面咱们别离介绍如上 3 个局部的初始化过程,这一篇只介绍模板表的初始化过程。
函数 TemplateTable::initialize()
的实现如下:
模板表 TemplateTable 保留了各个字节码的执行模板(指标代码生成函数和参数),而前一篇介绍对字节码的定义曾经进行了具体介绍,执行模板定义的是每个字节码如何在解释模式下执行的。initialize()函数的实现如下:
源代码地位:/src/share/vm/interpreter/templateInterpreter.cpp
void TemplateTable::initialize() {if (_is_initialized) return;
_bs = Universe::heap()->barrier_set();
// For better readability
const char _ = ' ';
const int ____ = 0;
const int ubcp = 1 << Template::uses_bcp_bit;
const int disp = 1 << Template::does_dispatch_bit;
const int clvm = 1 << Template::calls_vm_bit;
const int iswd = 1 << Template::wide_bit;
// interpr. templates
// Java spec bytecodes ubcp|disp|clvm|iswd in out generator argument
def(Bytecodes::_nop , ____|____|____|____, vtos, vtos, nop , _);
def(Bytecodes::_aconst_null , ____|____|____|____, vtos, atos, aconst_null , _);
def(Bytecodes::_iconst_m1 , ____|____|____|____, vtos, itos, iconst , -1);
def(Bytecodes::_iconst_0 , ____|____|____|____, vtos, itos, iconst , 0);
// ...
def(Bytecodes::_tableswitch , ubcp|disp|____|____, itos, vtos, tableswitch , _);
def(Bytecodes::_lookupswitch , ubcp|disp|____|____, itos, itos, lookupswitch , _);
def(Bytecodes::_ireturn , ____|disp|clvm|____, itos, itos, _return , itos);
def(Bytecodes::_lreturn , ____|disp|clvm|____, ltos, ltos, _return , ltos);
def(Bytecodes::_freturn , ____|disp|clvm|____, ftos, ftos, _return , ftos);
def(Bytecodes::_dreturn , ____|disp|clvm|____, dtos, dtos, _return , dtos);
def(Bytecodes::_areturn , ____|disp|clvm|____, atos, atos, _return , atos);
def(Bytecodes::_return , ____|disp|clvm|____, vtos, vtos, _return , vtos);
def(Bytecodes::_getstatic , ubcp|____|clvm|____, vtos, vtos, getstatic , f1_byte);
def(Bytecodes::_putstatic , ubcp|____|clvm|____, vtos, vtos, putstatic , f2_byte);
def(Bytecodes::_getfield , ubcp|____|clvm|____, vtos, vtos, getfield , f1_byte);
def(Bytecodes::_putfield , ubcp|____|clvm|____, vtos, vtos, putfield , f2_byte);
def(Bytecodes::_invokevirtual , ubcp|disp|clvm|____, vtos, vtos, invokevirtual , f2_byte);
def(Bytecodes::_invokespecial , ubcp|disp|clvm|____, vtos, vtos, invokespecial , f1_byte);
def(Bytecodes::_invokestatic , ubcp|disp|clvm|____, vtos, vtos, invokestatic , f1_byte);
def(Bytecodes::_invokeinterface , ubcp|disp|clvm|____, vtos, vtos, invokeinterface , f1_byte);
def(Bytecodes::_invokedynamic , ubcp|disp|clvm|____, vtos, vtos, invokedynamic , f1_byte);
def(Bytecodes::_new , ubcp|____|clvm|____, vtos, atos, _new , _);
def(Bytecodes::_newarray , ubcp|____|clvm|____, itos, atos, newarray , _);
def(Bytecodes::_anewarray , ubcp|____|clvm|____, itos, atos, anewarray , _);
def(Bytecodes::_arraylength , ____|____|____|____, atos, itos, arraylength , _);
def(Bytecodes::_athrow , ____|disp|____|____, atos, vtos, athrow , _);
def(Bytecodes::_checkcast , ubcp|____|clvm|____, atos, atos, checkcast , _);
def(Bytecodes::_instanceof , ubcp|____|clvm|____, atos, itos, instanceof , _);
def(Bytecodes::_monitorenter , ____|disp|clvm|____, atos, vtos, monitorenter , _);
def(Bytecodes::_monitorexit , ____|____|clvm|____, atos, vtos, monitorexit , _);
def(Bytecodes::_wide , ubcp|disp|____|____, vtos, vtos, wide , _);
def(Bytecodes::_multianewarray , ubcp|____|clvm|____, vtos, atos, multianewarray , _);
def(Bytecodes::_ifnull , ubcp|____|clvm|____, atos, vtos, if_nullcmp , equal);
def(Bytecodes::_ifnonnull , ubcp|____|clvm|____, atos, vtos, if_nullcmp , not_equal);
def(Bytecodes::_goto_w , ubcp|____|clvm|____, vtos, vtos, goto_w , _);
def(Bytecodes::_jsr_w , ubcp|____|____|____, vtos, vtos, jsr_w , _);
// wide Java spec bytecodes
def(Bytecodes::_iload , ubcp|____|____|iswd, vtos, itos, wide_iload , _);
def(Bytecodes::_lload , ubcp|____|____|iswd, vtos, ltos, wide_lload , _);
// ...
// JVM bytecodes
// ...
def(Bytecodes::_shouldnotreachhere , ____|____|____|____, vtos, vtos, shouldnotreachhere , _);
}
TemplateTable 的初始化调用 def()将所有字节码的指标代码生成函数和参数保留在_template_table 或_template_table_wide(wide 指令)模板数组中。除了虚拟机标准自身定义的字节码指令外,HotSpot 虚拟机也定义了一些字节码指令,这些指令为了辅助虚拟机进行更好的性能实现,例如 Bytecodes::_return_register_finalizer 等在之前曾经介绍过,能够更好的实现 finalizer 类型对象的注册性能。
咱们只给出局部字节码指令的模板定义,调用 def()函数对每个字节码指令的模板进行定义,传递的参数是咱们关注的重点:
(1)指出为哪个字节码指令定义模板
(2)ubcp|disp|clvm|iswd,这是一个组合数字,具体的数字与 Template 中定义的枚举类严密相干,枚举类中定义的常量如下:
enum Flags {
uses_bcp_bit, // set if template needs the bcp pointing to bytecode
does_dispatch_bit, // set if template dispatches on its own 就其自身而言; 靠本人
calls_vm_bit, // set if template calls the vm
wide_bit // set if template belongs to a wide instruction
};
上面具体解释这几个参数,如下:
- uses_bcp_bit,标记须要应用字节码指针(byte code pointer,数值为字节码基址 + 字节码偏移量)。示意生成的模板代码中是否须要应用指向字节码指令的指针,其实也就是说是否须要读取字节码指令的操作数,所以含有操作数的指令大部分都须要 bcp,然而有一些是不须要的,如 monitorenter 与 monitorexit 等,这些的操作数都在表达式栈中,表达式栈顶就是其操作数,并不需要从 Class 文件中读取,所以不须要 bcp;
- does_dispatch_bit,标记示意本人是否含有控制流转发逻辑,如 tableswitch、lookupswitch、invokevirtual、ireturn 等字节码指令,自身就须要进行控制流转发;
- calls_vm_bit,标记是否须要调用 JVM 函数,在调用 TemplateTable::call_VM()函数时都会判断是否有这个标记,通常办法调用 JVM 函数时都会通过调用 TemplateTable::call_VM()函数来间接实现调用。JVM 函数就是用 C ++ 写的函数。
- wide_bit,标记是否是 wide 指令(应用附加字节扩大全局变量索引)
(3)_tos_in 与_tos_out:示意模板执行前与模板执行后的 TosState(操作数栈栈顶元素的数据类型,TopOfStack,用来查看模板所申明的输入输出类型是否和该函数统一,以确保栈顶元素被正确应用)。
_tos_in 与_tos_out 的值必须是枚举类中定义的常量,如下:
enum TosState { // describes the tos cache contents
btos = 0, // byte, bool tos cached
ctos = 1, // char tos cached
stos = 2, // short tos cached
itos = 3, // int tos cached
ltos = 4, // long tos cached
ftos = 5, // float tos cached
dtos = 6, // double tos cached
atos = 7, // object cached
vtos = 8, // tos not cached
number_of_states,
ilgl // illegal state: should not occur
};
如 iload 指令,执行之前栈顶状态为 vtos,示意并不会应用栈顶的数据,所以如果程序为了进步执行效率将上一次执行的后果缓存到了寄存器中,那么此时就应该在执行 iload 指令之前将这个寄存器的值压入栈顶。iload 指令执行之后的栈顶状态为 itos,因为 iload 是向操作数栈中压入一个整数,所以此时的栈顶状态为 int 类型,那么这个值能够缓存到寄存器中,假如下一个指令为 ireturn,那么栈顶之前与之后的状态别离为 itos 和 itos,那么可间接将缓存在寄存器中的 int 类型返回即可,不须要做任何和操作数栈相干的操作。
(4)_gen 与_arg:_gen 示意模板生成器(函数指针),这个函数会为对应的字节码生成对应的执行逻辑;_arg 示意为模板生成器传递的参数。调用函数指针会为每个字节码指令按其语义针对不同的平台上生成不同的机器指令,这里咱们只探讨 x86 架构下 64 位的机器指令实现,因为机器指令很难读懂,所以咱们后续只浏览由机器指令反编译的汇编指令。
上面看一下 TemplateTable::initialize()函数中调用的 Template::def()函数,如下:
void TemplateTable::def(
Bytecodes::Code code, // 字节码指令
int flags, // 标记位
TosState in, // 模板执行前 TosState
TosState out, // 模板执行后 TosState
void (*gen)(int arg), // 模板生成器,是模板的外围组件
int arg
) {
// 示意是否须要 bcp 指针
const int ubcp = 1 << Template::uses_bcp_bit;
// 示意是否在模板范畴内进行转发
const int disp = 1 << Template::does_dispatch_bit;
// 示意是否须要调用 JVM 函数
const int clvm = 1 << Template::calls_vm_bit;
// 示意是否为 wide 指令
const int iswd = 1 << Template::wide_bit;
// 如果是容许在字节码指令前加 wide 字节码指令的一些指令,那么
// 会应用_template_table_wild 模板数组进行字节码转发,否则
// 应用_template_table 模板数组进行转发
bool is_wide = (flags & iswd) != 0;
Template* t = is_wide ? template_for_wide(code) : template_for(code);
// 调用模板表 t 的 initialize()办法初始化模板表
t->initialize(flags, in, out, gen, arg);
}
模板表由模板表数组与一组生成器组成:
模板数组有_template_table 与_template_table_wild,数组的下标为字节码的 Opcode,值为 Template。定义如下:
Template TemplateTable::_template_table[Bytecodes::number_of_codes];
Template TemplateTable::_template_table_wide[Bytecodes::number_of_codes];
模板数组的值为 Template,这个 Template 类中定义了保留标记位 flags 的_flags 属性,保留栈顶缓存状态 in 和 out 的_tos_in 和_tos_out,还有保留生成器 gen 及参数 arg 的_gen 与_arg,所以调用 t ->initialize()后其实是初始化 Template 中的变量。initialize()函数的实现如下:
void Template::initialize(
int flags,
TosState tos_in,
TosState tos_out,
generator gen,
int arg
) {
_flags = flags;
_tos_in = tos_in;
_tos_out = tos_out;
_gen = gen;
_arg = arg;
}
不过这里并不会调用 gen 函数生成对应的汇编代码,只是将传递给 def()函数的各种信息保留到 Template 实例中,在 TemplateTable::def()函数中,通过 template_for()或 template_for_wild()函数获取到数组中对应的 Template 实例后,就会调用 Template::initialize()函数将信息保留到对应的 Template 实例中,这样就能够依据字节码索引从数组中获取对应的 Template 实例,进而获取字节码指令模板的相干信息。
尽管这里并不会调用 gen 来生成字节码指令对应的机器指令,然而咱们能够提前看一下 gen 这个指针函数是怎么为某个字节码指令生成对应的机器指令的。
看一下 TemplateTable::initialize()函数中对 def()函数的调用,以_iinc(将局部变量表中对应的 slot 位的值减少 1)为例,调用如下:
def(
Bytecodes::_iinc, // 字节码指令
ubcp|____|clvm|____, // 标记
vtos, // 模板执行前的 TosState
vtos, // 模板执行后的 TosState
iinc , // 模板生成器,是一个 iinc()函数的指针
_ // 不须要模板生成器参数
);
设置标记位 uses_bcp_bit 和 calls_vm_bit,示意 iinc 指令的生成器须要应用 bcp 指针函数 at_bcp(),且须要调用 JVM 函数,上面给出了生成器的定义:
源代码地位:/hotspot/src/cpu/x86/vm/templateTable_x86_64.cpp
void TemplateTable::iinc() {transition(vtos, vtos);
__ load_signed_byte(rdx, at_bcp(2)); // get constant
locals_index(rbx);
__ addl(iaddress(rbx), rdx);
}
因为 iinc 指令只波及到对局部变量表的操作,并不会影响操作数栈,也不须要应用操作数栈顶的值,所以栈顶之前与之后的状态为 vtos 与 vtos,调用 transition()函数只是验证栈顶缓存的状态是否正确。
iinc 指令的字节码格局如下:
iinc
index // 局部变量表索引值
const // 将局部变量表索引值对应的 slot 值加 const
操作码 iinc 占用一个字节,而 index 与 const 别离占用一个字节。应用 at_bcp()函数获取 iinc 指令的操作数,2 示意偏移 2 字节,所以会将 const 取出来存储到 rdx 中。调用 locals_index()函数取出 index,locals_index()就是 JVM 函数。最终生成的汇编如下:
// %r13 存储的是指向字节码的指针,偏移
// 2 字节后取出 const 存储到 %edx
movsbl 0x2(%r13),%edx
// 取出 index 存储到 %ebx
movzbl 0x1(%r13),%ebx
neg %rbx
// %r14 指向本地变量表的首地址,将 %edx 加到
// %r14+%rbx* 8 指向的内存所存储的值上
// 之所以要对 %rbx 执行 neg 进行符号反转,// 是因为在 Linux 内核的操作系统上,// 栈是向低地址方向成长的
add %edx,(%r14,%rbx,8)
正文解释的曾经十分分明了,这里不再过多介绍。
第 11 篇 - 意识 Stub 与 StubQueue
在 第 10 篇 - 初始化模板表 咱们介绍过 TemplateInterpreter::initialize()函数,在这个函数中会调用 TemplateTable::initialize()函数初始化模板表,随后会应用 new 关键字初始化定义在 AbstractInterpreter 类中的_code 动态属性,如下:
static StubQueue* _code;
因为 TemplateInterpreter 继承自 AbstractInterpreter,所以在 TemplateInterpreter 中初始化的_code 属性其实就是 AbstractInterpreter 类中定义的_code 属性。
在 initialize()函数中初始化_code 变量的代码如下:
// InterpreterCodeSize 是在平台相干
// 的 templateInterpreter_x86.hpp 中
// 定义的,64 位下是 256 * 1024
int code_size = InterpreterCodeSize;
_code = new StubQueue(
new InterpreterCodeletInterface,
code_size,
NULL,
"Interpreter");
StubQueue 是用来保留生成的本地代码的 Stub 队列,队列每一个元素对应一个 InterpreterCodelet 对象,InterpreterCodelet 对象继承自形象基类 Stub,蕴含了字节码对应的本地代码以及一些调试和输入信息。上面咱们介绍一下 StubQueue 类及相干类 Stub、InterpreterCodelet 类和 CodeletMark 类。
1、InterpreterCodelet 与 Stub 类
Stub 类的定义如下:
class Stub VALUE_OBJ_CLASS_SPEC {...};
InterpreterCodelet 类继承自 Stub 类,具体的定义如下:
class InterpreterCodelet: public Stub {
private:
int _size; // the size in bytes
const char* _description; // a description of the codelet, for debugging & printing
Bytecodes::Code _bytecode; // associated bytecode if any
public:
// Code info
address code_begin() const {return (address)this + round_to(sizeof(InterpreterCodelet), CodeEntryAlignment);
}
address code_end() const {return (address)this + size();}
int size() const {return _size;}
// ...
int code_size() const {return code_end() - code_begin();}
// ...
};
InterpreterCodelet 实例存储在 StubQueue 中,每个 InterpreterCodelet 实例都代表一段机器指令(蕴含了字节码对应的机器指令片段以及一些调试和输入信息),如每个字节码都有一个 InterpreterCodelet 实例,所以在解释执行时,如果要执行某个字节码,则执行的就是由 InterpreterCodelet 实例代表的机器指令片段。
类中定义了 3 个属性及一些函数,其内存布局如下图所示。
在对齐至 CodeEntryAlignment 后,紧接着 InterpreterCodelet 的就是生成的指标代码。
2、StubQueue 类
StubQueue 是用来保留生成的本地机器指令片段的 Stub 队列,队列每一个元素都是一个 InterpreterCodelet 实例。
StubQueue 类的定义如下:
class StubQueue: public CHeapObj<mtCode> {
private:
StubInterface* _stub_interface; // the interface prototype
address _stub_buffer; // where all stubs are stored
int _buffer_size; // the buffer size in bytes
int _buffer_limit; // the (byte) index of the actual buffer limit (_buffer_limit <= _buffer_size)
int _queue_begin; // the (byte) index of the first queue entry (word-aligned)
int _queue_end; // the (byte) index of the first entry after the queue (word-aligned)
int _number_of_stubs; // the number of buffered stubs
bool is_contiguous() const {return _queue_begin <= _queue_end;}
int index_of(Stub* s) const {int i = (address)s - _stub_buffer;
return i;
}
Stub* stub_at(int i) const {return (Stub*)(_stub_buffer + i);
}
Stub* current_stub() const {return stub_at(_queue_end);
}
// ...
}
这个类的构造函数如下:
StubQueue::StubQueue(
StubInterface* stub_interface, // InterpreterCodeletInterface 对象
int buffer_size, // 256*1024
Mutex* lock,
const char* name) : _mutex(lock)
{intptr_t size = round_to(buffer_size, 2*BytesPerWord); // BytesPerWord 的值为 8
BufferBlob* blob = BufferBlob::create(name, size); // 在 StubQueue 中创立 BufferBlob 对象
_stub_interface = stub_interface;
_buffer_size = blob->content_size();
_buffer_limit = blob->content_size();
_stub_buffer = blob->content_begin();
_queue_begin = 0;
_queue_end = 0;
_number_of_stubs = 0;
}
stub_interface 用来保留一个 InterpreterCodeletInterface 类型的实例,InterpreterCodeletInterface 类中定义了操作 Stub 的函数,防止了在 Stub 中定义虚函数。每个 StubQueue 都有一个 InterpreterCodeletInterface,能够通过这个来操作 StubQueue 中存储的每个 Stub 实例。
调用 BufferBlob::create()函数为 StubQueue 分配内存,这里咱们须要记住 StubQueue 用的内存是通过 BufferBlob 调配进去的,也就是 BufferBlob 其本质可能是一个 StubQueue。上面就来具体介绍下 create()函数。
BufferBlob* BufferBlob::create(const char* name, int buffer_size) {
// ...
BufferBlob* blob = NULL;
unsigned int size = sizeof(BufferBlob);
// align the size to CodeEntryAlignment
size = align_code_offset(size);
size += round_to(buffer_size, oopSize); // oopSize 是一个指针的宽度,在 64 位上就是 8
{MutexLockerEx mu(CodeCache_lock, Mutex::_no_safepoint_check_flag);
blob = new (size) BufferBlob(name, size);
}
return blob;
}
通过 new 关键字为 BufferBlob 分配内存,new 重载运算符如下:
void* BufferBlob::operator new(size_t s, unsigned size, bool is_critical) throw() {void* p = CodeCache::allocate(size, is_critical);
return p;
}
从 codeCache 中分配内存,CodeCache 应用的是本地内存,有本人的内存治理方法,在前面将会具体介绍。
StubQueue 的布局构造如下图所示。
队列中的 InterpreterCodelet 示意一个小例程,比方 iconst_1 对应的机器码,invokedynamic 对应的机器码,异样解决对应的代码,办法入口点对应的代码,这些代码都是一个个 InterpreterCodelet。整个解释器都是由这些小块代码例程组成的,每个小块例程实现解释器的局部性能,以此实现整个解释器。
第 12 篇 - 意识 CodeletMark
InterpreterCodelet 依赖 CodeletMark 实现主动创立和初始化。CodeletMark 继承自 ResourceMark,容许主动析构,执行的次要操作就是,会依照 InterpreterCodelet 中存储的理论机器指令片段分配内存并提交。这个类的定义如下:
class CodeletMark: ResourceMark {
private:
InterpreterCodelet* _clet; // InterpreterCodelet 继承自 Stub
InterpreterMacroAssembler** _masm;
CodeBuffer _cb;
public:
// 构造函数
CodeletMark(
InterpreterMacroAssembler*& masm,
const char* description,
Bytecodes::Code bytecode = Bytecodes::_illegal):
// AbstractInterpreter::code()获取的是 StubQueue* 类型的值,调用 request()办法获取的
// 是 Stub* 类型的值,调用的 request()办法实现在 vm/code/stubs.cpp 文件中
_clet((InterpreterCodelet*)AbstractInterpreter::code()->request(codelet_size()) ),
_cb(_clet->code_begin(), _clet->code_size())
{
// 初始化 InterpreterCodelet 中的_description 和_bytecode 属性
_clet->initialize(description, bytecode);
// InterpreterMacroAssembler->MacroAssembler->Assembler->AbstractAssembler
// 通过传入的 cb.insts 属性的值来初始化 AbstractAssembler 的_code_section 与_oop_recorder 属性的值
// create assembler for code generation
masm = new InterpreterMacroAssembler(&_cb); // 在构造函数中,初始化 r13 指向 bcp、r14 指向本地局部变量表
_masm = &masm;
}
// ... 省略析构函数
};
在构造函数中次要实现 2 个工作:
(1)初始化 InterpreterCodelet 类型的变量_clet。对 InterpreterCodelet 实例中的 3 个属性赋值;
(2)创立一个 InterpreterMacroAssembler 实例并赋值给 masm 与_masm,此实例会通过 CodeBuffer 向 InterpreterCodelet 实例写入机器指令。
在析构函数中,通常在代码块完结时会主动调用析构函数,在析构函数中实现 InterpreterCodelet 应用的内存的提交并清理相干变量的值。
1、CodeletMark 构造函数
在 CodeletMark 构造函数会从 StubQueue 中为 InterpreterCodelet 分配内存并初始化相干变量
在初始化_clet 变量时,调用 AbstractInterpreter::code()办法返回 AbstractInterpreter 类的_code 属性的值,这个值在之前 TemplateInterpreter::initialize()办法中曾经初始化了。持续调用 StubQueue 类中的 request()办法,传递的就是要求调配的用来存储 code 的大小,通过调用 codelet_size()函数来获取,如下:
int codelet_size() {// Request the whole code buffer (minus a little for alignment).
// The commit call below trims it back for each codelet.
int codelet_size = AbstractInterpreter::code()->available_space() - 2*K;
return codelet_size;
}
须要留神,在创立 InterpreterCodelet 时,会将 StubQueue 中剩下的简直所有可用的内存都调配给此次的 InterpreterCodelet 实例,这必然会有很大的节约,不过咱们在析构函数中会依照 InterpreterCodelet 实例的实例大小提交内存的,所以不必放心节约这个问题。这么做的次要起因就是让各个 InterpreterCodelet 实例在内存中间断寄存,这样有一个十分重要的利用,那就是只有简略通过 pc 判断就可晓得栈帧是否为解释栈帧了,前面将会具体介绍。
通过调用 StubQueue::request()函数从 StubQueue 中分配内存。函数的实现如下:
Stub* StubQueue::request(int requested_code_size) {Stub* s = current_stub();
int x = stub_code_size_to_size(requested_code_size);
int requested_size = round_to(x , CodeEntryAlignment); // CodeEntryAlignment=32
// 比拟须要为新的 InterpreterCodelet 调配的内存和可用内存的大小状况
if (requested_size <= available_space()) {if (is_contiguous()) { // 判断_queue_begin 小于等于_queue_end 时,函数返回 true
// Queue: |...|XXXXXXX|.............|
// ^0 ^begin ^end ^size = limit
assert(_buffer_limit == _buffer_size, "buffer must be fully usable");
if (_queue_end + requested_size <= _buffer_size) {// code fits in(适应) at the end => nothing to do
CodeStrings strings;
stub_initialize(s, requested_size, strings);
return s; // 如果够的话就间接返回
} else {
// stub doesn't fit in at the queue end
// => reduce buffer limit & wrap around
assert(!is_empty(), "just checkin'");
_buffer_limit = _queue_end;
_queue_end = 0;
}
}
}
// ...
return NULL;
}
通过如上的函数,咱们可能分明看到如何从 StubQueue 中调配 InterpreterCodelet 内存的逻辑。
首先计算此次须要从 StubQueue 中调配的内存大小,调用的相干函数如下:
调用的 stub_code_size_to_size()函数的实现如下:
// StubQueue 类中定义的函数
int stub_code_size_to_size(int code_size) const {return _stub_interface->code_size_to_size(code_size);
}
// InterpreterCodeletInterface 类中定义的函数
virtual int code_size_to_size(int code_size) const {return InterpreterCodelet::code_size_to_size(code_size);
}
// InterpreterCodelet 类中定义的函数
static int code_size_to_size(int code_size) {
// CodeEntryAlignment = 32
// sizeof(InterpreterCodelet) = 32
return round_to(sizeof(InterpreterCodelet), CodeEntryAlignment) + code_size;
}
通过如上的分配内存大小的形式可知内存构造如下:
在 StubQueue::request()函数中计算出须要从 StubQueue 中调配的内存大小后,上面进行内存调配。StubQueue::request()函数只给出了最个别的状况,也就是假如所有的 InterpreterCodelet 实例都是从 StubQueue 的_stub_buffer 地址开始间断调配的。is_contiguous()函数用来判断区域是否间断,实现如下:
bool is_contiguous() const {return _queue_begin <= _queue_end;}
调用的 available_space()函数失去 StubQueue 可用区域的大小,实现如下:
// StubQueue 类中定义的办法
int available_space() const {
int d = _queue_begin - _queue_end - 1;
return d < 0 ? d + _buffer_size : d;
}
调用如上函数后失去的大小为下图的黄色区域局部。
持续看 StubQueue::request()函数,当能满足此次 InterpreterCodelet 实例要求的内存大小时,会调用 stub_initialize()函数,此函数的实现如下:
// 上面都是通过 stubInterface 来操作 Stub 的
void stub_initialize(Stub* s, int size,CodeStrings& strings) {// 通过_stub_interface 来操作 Stub, 会调用 s 的 initialize()函数
_stub_interface->initialize(s, size, strings);
}
// 定义在 InterpreterCodeletInterface 类中函数
virtual void initialize(Stub* self, int size,CodeStrings& strings){cast(self)->initialize(size, strings);
}
// 定义在 InterpreterCodelet 类中的函数
void initialize(int size,CodeStrings& strings) {_size = size;}
咱们通过 StubInterface 类中定义的函数来操作 Stub,至于为什么要通过 StubInterface 来操作 Stub,就是因为 Stub 实例很多,所以为了防止在 Stub 中写虚函数(C++ 中对含有虚函数的类须要调配一个指针的空间指向虚函数表)节约内存空间而采取的方法。
如上 3 个函数最终只实现了一件事儿,就是将此次调配到的内存大小记录在 InterpreterCodelet 的_size 属性中。后面在介绍函数 codelet_size()时提到过,这个值在存储了机器指令片段后通常还会空余很多空间,不过不要焦急,上面要介绍的析构函数会依据 InterpreterCodelet 实例中理论生成的机器指令的大小更新这个属性值。
2、CodeletMark 析构函数
析构函数的实现如下:
// 析构函数
~CodeletMark() {
// 对齐 InterpreterCodelet
(*_masm)->align(wordSize);
// 确保生成的所有机器指令片段都存储到了 InterpreterCodelet 实例中
(*_masm)->flush();
// 更新 InterpreterCodelet 实例的相干属性值
AbstractInterpreter::code()->commit((*_masm)->code()->pure_insts_size(), (*_masm)->code()->strings());
// 设置_masm,这样就无奈通过这个值持续向此 InterpreterCodelet 实例中生成机器指令了
*_masm = NULL;
}
调用 AbstractInterpreter::code()函数获取 StubQueue。调用 (*_masm)->code()->pure_insts_size() 获取的就是 InterpreterCodelet 实例的机器指令片段理论须要的内存大小。
StubQueue::commit()函数的实现如下:
void StubQueue::commit(int committed_code_size, CodeStrings& strings) {int x = stub_code_size_to_size(committed_code_size);
int committed_size = round_to(x, CodeEntryAlignment);
Stub* s = current_stub();
assert(committed_size <= stub_size(s), "committed size must not exceed requested size");
stub_initialize(s, committed_size, strings);
_queue_end += committed_size;
_number_of_stubs++;
}
调用 stub_initialize()函数通过 InterpreterCodelet 实例的_size 属性记录此实例中机器指令片段理论内存大小。同时更新 StubQueue 的_queue_end 和_number_of_stubs 属性的值,这样就能够为下次 InterpreterCodelet 实例持续分配内存了。
第 13 篇 - 通过 InterpreterCodelet 存储机器指令片段
在 TemplateInterpreterGenerator::generate_all()函数中生成了许多字节码指令以及一些虚拟机辅助执行的机器指令片段,例如生成空指针异样抛出入口的实现如下:
{CodeletMark cm(_masm, "throw exception entrypoints");
// ...
Interpreter::_throw_NullPointerException_entry = generate_exception_handler("java/lang/NullPointerException",NULL);
// ...
}
调用 generate_exception_handler()函数生成抛出空指针的代码片段。
address generate_exception_handler(const char* name, const char* message) {return generate_exception_handler_common(name, message, false);
}
调用的 generate_exception_handler_common()函数的实现如下:
address TemplateInterpreterGenerator::generate_exception_handler_common(
const char* name,
const char* message,
bool pass_oop
) {assert(!pass_oop || message == NULL, "either oop or message but not both");
address entry = __ pc();
if (pass_oop) {
// object is at TOS
__ pop(c_rarg2);
}
// expression stack must be empty before entering the VM if an
// exception happened
__ empty_expression_stack();
// setup parameters
__ lea(c_rarg1, ExternalAddress((address)name));
if (pass_oop) {
__ call_VM(rax,
CAST_FROM_FN_PTR(address,InterpreterRuntime::create_klass_exception),
c_rarg1,c_rarg2);
} else {
// kind of lame ExternalAddress can't take NULL because
// external_word_Relocation will assert.
if (message != NULL) {__ lea(c_rarg2, ExternalAddress((address)message));
} else {__ movptr(c_rarg2, NULL_WORD);
}
__ call_VM(rax,
CAST_FROM_FN_PTR(address, InterpreterRuntime::create_exception),
c_rarg1, c_rarg2);
}
// throw exception
__ jump(ExternalAddress(Interpreter::throw_exception_entry()));
return entry;
}
生成的汇编代码如下:
0x00007fffe10101cb: mov -0x40(%rbp),%rsp
0x00007fffe10101cf: movq $0x0,-0x10(%rbp)
0x00007fffe10101d7: movabs $0x7ffff6e09878,%rsi
0x00007fffe10101e1: movabs $0x0,%rdx
0x00007fffe10101eb: callq 0x00007fffe10101f5
0x00007fffe10101f0: jmpq 0x00007fffe1010288
0x00007fffe10101f5: lea 0x8(%rsp),%rax
0x00007fffe10101fa: mov %r13,-0x38(%rbp)
0x00007fffe10101fe: mov %r15,%rdi
0x00007fffe1010201: mov %rbp,0x200(%r15)
0x00007fffe1010208: mov %rax,0x1f0(%r15)
0x00007fffe101020f: test $0xf,%esp
0x00007fffe1010215: je 0x00007fffe101022d
0x00007fffe101021b: sub $0x8,%rsp
0x00007fffe101021f: callq 0x00007ffff66b3fbc
0x00007fffe1010224: add $0x8,%rsp
0x00007fffe1010228: jmpq 0x00007fffe1010232
0x00007fffe101022d: callq 0x00007ffff66b3fbc
0x00007fffe1010232: movabs $0x0,%r10
0x00007fffe101023c: mov %r10,0x1f0(%r15)
0x00007fffe1010243: movabs $0x0,%r10
0x00007fffe101024d: mov %r10,0x200(%r15)
0x00007fffe1010254: cmpq $0x0,0x8(%r15)
0x00007fffe101025c: je 0x00007fffe1010267
0x00007fffe1010262: jmpq 0x00007fffe1000420
0x00007fffe1010267: mov 0x250(%r15),%rax
0x00007fffe101026e: movabs $0x0,%r10
0x00007fffe1010278: mov %r10,0x250(%r15)
0x00007fffe101027f: mov -0x38(%rbp),%r13
0x00007fffe1010283: mov -0x30(%rbp),%r14
0x00007fffe1010287: retq
0x00007fffe1010288: jmpq 0x00007fffe100f3d3
在这里的重点不是读懂 TemplateInterpreterGenerator::generate_exception_handler_common()函数的逻辑及生成的汇编代码,而是要分明晓得 CodeletMark 的利用,以及 generate_exception_handler_common()函数生成的机器指令是如何写入 InterpreterCodelet 实例中的。之前介绍过 InterpreterCodelet 与 CodeBuffer 类,如下:
通过 CodeBuffer 操作 InterpreterCodelet 实例的存储机器指令片段的内存区域,而 CodeBuffer 中的代码局部(CodeSection)被赋值给 AbstractAssembler::_code_section。这样咱们就能够通过_code_section 属性向 InterpreterCodelet 实例中写入机器指令了。
向 CodeletMark 中传入的_masm 参数定义在 AbstractInterpreterGenerator 类中,如下:
class AbstractInterpreterGenerator: public StackObj {
protected:
InterpreterMacroAssembler* _masm;
// ...
}
generate_exception_handler_common()函数中的__是一个宏,定义如下:
#define __ _masm->
这样其实就是调用 InterpreterMacroAssembler 类中的相干函数写机器指令,例如
__ pop(c_rarg2);
调用的 pop()函数如下:
// 定义在 InterpreterMacroAssembler 中
void pop(Register r) {((MacroAssembler*)this)->pop(r);
}
// 定义在 Assembler 类中
void Assembler::pop(Register dst) {int encode = prefix_and_encode(dst->encoding());
emit_int8(0x58 | encode);
}
// 定义在 AbstractAssembler 类中
void emit_int8(int8_t x) {code_section()->emit_int8(x);
}
code_section()函数获取的就是 AbstractAssembler 的_code_section 属性的值。
第 14 篇 - 生成重要的例程
之前介绍过 TemplateInterpreter::initialize()函数,在这个函数中初始化了模板表和 StubQueue 实例,通过如下形式创立 InterpreterGenerator 实例:
InterpreterGenerator g(_code);
在创立 InterpreterGenerator 实例时会调用 generate_all()函数,如下:
InterpreterGenerator::InterpreterGenerator(StubQueue* code)
: TemplateInterpreterGenerator(code) {generate_all();
}
在 generate_all()函数中生成各种例程(机器指令片段)并存储到 Interpretercodelet 实例中。在 HotSpot VM 中,不仅有字节码对应的例程,还有许多辅助虚拟机运行时的例程,如之前介绍的一般办法入口 entry_point 例程,解决异样的例程等等。这些例程都会存储到 StubQueue 中,如下图所示。
生成的一些重要例程如下表所示。
其中非本地办法的入口、本地办法的入口和字节码的入口比拟重要,也是咱们前面介绍的重点内容。这一篇介绍非本地办法的入口和字节码的入口,对于本地办法的入口将在介绍本地办法时具体介绍,这里不过多介绍。
1、非本地办法入口
咱们在之前介绍为非本地的一般办法创立 Java 栈帧的时候提到过,次要的非本地办法入口有如下几类:
enum MethodKind {
zerolocals, // 一般的办法
zerolocals_synchronized, // 一般的同步办法
...
}
在 generate_all()函数中生成一般办法和一般的同步办法的入口逻辑如下:
{CodeletMark cm(_masm, "method entry point (kind =" "zerolocals" ")");
Interpreter::_entry_table[Interpreter::zerolocals] = generate_method_entry(Interpreter::zerolocals);
}
{CodeletMark cm(_masm, "method entry point (kind =" "zerolocals_synchronized" ")");
Interpreter::_entry_table[Interpreter::zerolocals_synchronized] = generate_method_entry(Interpreter::zerolocals_synchronized);
}
调用的 generate_method_entry()函数在第 6 篇曾经具体介绍过,最终会生成创立 Java 栈帧的例程,将例程的首地址存储到 Interpreter::_entry_table 数组中。
对于同步办法的栈帧建设及非凡逻辑的解决将在介绍锁相干常识时具体介绍,这里不在过多介绍。
除了一般的办法外,还为一些办法生成了一些非凡的入口地址,如为 java.lang.Math.sin()、java.lang.Math.cos()等办法生成的例程。如果大家有趣味能够本人钻研一下,这里不在具体介绍。
2、字节码入口
在 generate_all()函数中会调用 set_entry_points_for_all_bytes()函数,此函数对所有被定义的字节码生成例程并通过对应的属性保留入口,这些入口指向了例程的首地址。set_entry_points_for_all_bytes()函数的实现如下:
void TemplateInterpreterGenerator::set_entry_points_for_all_bytes() {for (int i = 0; i < DispatchTable::length; i++) {Bytecodes::Code code = (Bytecodes::Code)i;
if (Bytecodes::is_defined(code)) {set_entry_points(code);
} else {set_unimplemented(i);
}
}
}
当 code 是 Java 虚拟机标准中定义的字节码指令时,调用 set_entry_points()函数,此函数取出该字节码指令对应的 Template 模板并调用 set_short_enrty_points()函数进行解决,将入口地址保留在转发表(DispatchTable)_normal_table 或_wentry_table(应用 wide 指令)中。Template 模板在之前曾经介绍过,字节码指令都会对应一个 Template 模板,而模板中保留着字节码指令生成对应代码例程中须要的信息。
set_entry_points()函数的实现如下:
void TemplateInterpreterGenerator::set_entry_points(Bytecodes::Code code) {CodeletMark cm(_masm, Bytecodes::name(code), code);
address bep = _illegal_bytecode_sequence;
address cep = _illegal_bytecode_sequence;
address sep = _illegal_bytecode_sequence;
address aep = _illegal_bytecode_sequence;
address iep = _illegal_bytecode_sequence;
address lep = _illegal_bytecode_sequence;
address fep = _illegal_bytecode_sequence;
address dep = _illegal_bytecode_sequence;
address vep = _unimplemented_bytecode;
address wep = _unimplemented_bytecode;
// 解决非 wide 指令,留神指的是那些不能在后面加 wide 指令的字节码指令
if (Bytecodes::is_defined(code)) {Template* t = TemplateTable::template_for(code);
set_short_entry_points(t, bep, cep, sep, aep, iep, lep, fep, dep, vep);
}
// 解决 wide 指令,留神指的是那些能在后面加 wide 指令的字节码指令
if (Bytecodes::wide_is_defined(code)) {Template* t = TemplateTable::template_for_wide(code);
set_wide_entry_point(t, wep);
}
// 当为非 wide 指令时,共有 9 个入口,当为 wide 指令时,只有一个入口
EntryPoint entry(bep, cep, sep, aep, iep, lep, fep, dep, vep);
Interpreter::_normal_table.set_entry(code, entry);
Interpreter::_wentry_point = wep;
}
留神函数开始时申明时创立了一个变量 cm,此时会调用 CodeletMark 构造函数在 StubQueue 中创立出存储机器片段的 InterpreterCodelet 实例,所以调用 TemplateInterpreterGenerator::set_short_entry_points()等函数生成的机器指令都会写入到这个实例中。当函数执行实现后,CodeletMark 析构函数会提交应用的内存并重置相干属性值。
接下来就是为示意栈顶缓存(Top-of-Stack Caching,缩写为 TOSCA,简称 Tos)状态的变量赋初始值,其中的_illegal_bytecode_sequence 与_unimplemented_bytecode 变量指向的也是特定例程的入口地址,这些例程就是在 generate_all()函数中生成的,如果大家有趣味,能够钻研一下这些例程是怎么解决非法字节码等状况的。
调用 set_short_entry_points()函数时,须要传入栈顶缓存状态,也就是上一个字节码执行时可能会将产生的后果存储到寄存器中。应用栈顶缓存次要还是为了进步解释执行的效率。HotSpot VM 共定义了 9 种 TosState,通过枚举常量来示意,如下:
enum TosState { // describes the tos cache contents
btos = 0, // byte, bool tos cached
ctos = 1, // char tos cached
stos = 2, // short tos cached
itos = 3, // int tos cached
ltos = 4, // long tos cached
ftos = 5, // float tos cached
dtos = 6, // double tos cached
atos = 7, // object cached
vtos = 8, // tos not cached
number_of_states,
ilgl // illegal state: should not occur
};
以非 wide 指令为例进行阐明,bep(byte entry point)、cep、sep、aep、iep、lep、fep、dep、vep 别离示意指令执行前栈顶元素状态为 byte/boolean、char、short、array/reference(对象援用)、int、long、float、double、void 类型时的入口地址。举个例子,如 iconst_0 示意向栈中压入常量 0,那么字节码指令模板中有如下定义:
def(Bytecodes::_iconst_0 , ____|____|____|____, vtos, itos, iconst,0);
第 3 个参数指明了 tos_in,第 4 个参数为 tos_out,tos_in 与 tos_out 是指令执行前后的 TosState。也就是说,执行此字节码指令之前不须要获取栈顶缓存的值,所以为 void;执行实现后栈顶会缓存一个 int 类型的整数,也就是 0。缓存通常会缓存到寄存器中,所以比起压入栈中,获取的效率要更高一些,如果下一个执行的字节码指令不须要,那么还须要将缓存的 0 值压入栈内。假如下一个执行的字节码也为 iconst,那么要从 iconst 指令的 iep(上一个缓存了 int 类型整数 0)入口来执行,因为 iconst 的入口要求为 vtos,所以须要将寄存器中的 int 类型数值 0 入栈。所以每个字节码指令都会有多个入口,这样任何一个字节码指令在执行实现后,都能够依据以后执行后的栈顶缓存状态找到下一个须要执行字节码的对应入口。
再回头看一下咱们第 8 篇介绍的散发字节码相干内容,为各个字节码设置入口的函数 DispatchTable::set_entry(),其中的_table 的一维为栈顶缓存状态,二维为 Opcode,通过这 2 个维度可能找到一段机器指令,这就是依据以后的栈顶缓存状态定位到的字节码须要执行的例程。咱们看一下 TemplateInterpreterGenerator::set_entry_points()函数,最初会调用 DispatchTable::set_entry()函数为_table 属性赋值。这样类型为 DispatchTable 的 TemplateInterpreter::_normal_table 与 TemplateInterpreter::_wentry_point 变量就能够实现字节码散发了。
调用 TemplateTable::template_for()函数能够从 TemplateTable::_template_table 数组中获取对应的 Template 实例,而后调用 set_short_entry_points()函数生成例程。非 wild 指令调用 set_short_entry_points()函数,set_short_entry_points()函数的实现如下:
void TemplateInterpreterGenerator::set_short_entry_points(
Template* t,
address& bep, address& cep, address& sep, address& aep, address& iep,
address& lep, address& fep, address& dep, address& vep
) {switch (t->tos_in()) {
case btos:
case ctos:
case stos:
ShouldNotReachHere();
break;
case atos: vep = __ pc(); __ pop(atos); aep = __ pc(); generate_and_dispatch(t); break;
case itos: vep = __ pc(); __ pop(itos); iep = __ pc(); generate_and_dispatch(t); break;
case ltos: vep = __ pc(); __ pop(ltos); lep = __ pc(); generate_and_dispatch(t); break;
case ftos: vep = __ pc(); __ pop(ftos); fep = __ pc(); generate_and_dispatch(t); break;
case dtos: vep = __ pc(); __ pop(dtos); dep = __ pc(); generate_and_dispatch(t); break;
case vtos: set_vtos_entry_points(t, bep, cep, sep, aep, iep, lep, fep, dep, vep); break;
default : ShouldNotReachHere(); break;}
}
set_short_entry_points()函数会依据 Template 实例中保留的字节码模板信息生成最多 9 个栈顶入口并赋值给传入参数 bep、cep 等,也就是给 Template 代表的特定字节码指令生成相应的入口地址。
set_short_entry_points()函数依据操作数栈栈顶元素类型进行判断,首先 byte、char 和 short 类型都应被当做 int 类型进行解决,所以不会为字节码指令生成这几个类型的入口地址;如果以后字节码执行之前要求有栈顶元素并且类型是 atos 对象类型,那么当没有栈顶缓存时,从 vep 入口进入,而后弹出表达式栈中的对象到栈顶缓存寄存器后,就能够间接从 aep 进入,itos、ltos、ftos 和 dtos 也都相似,会别离成为 2 个入口地址;如果不要求有栈顶元素,那么就是 vtos,非 void 类型将调用 generate_and_dispatch()函数生成各种入口。
set_vtos_entry_points()函数的实现如下:
void TemplateInterpreterGenerator::set_vtos_entry_points(
Template* t,
address& bep,
address& cep,
address& sep,
address& aep,
address& iep,
address& lep,
address& fep,
address& dep,
address& vep) {
Label L;
aep = __ pc(); __ push_ptr(); __ jmp(L);
fep = __ pc(); __ push_f(); __ jmp(L);
dep = __ pc(); __ push_d(); __ jmp(L);
lep = __ pc(); __ push_l(); __ jmp(L);
bep = cep = sep =
iep = __ pc(); __ push_i();
vep = __ pc();
__ bind(L);
generate_and_dispatch(t);
}
如果字节码不要求有栈顶缓存时(即 vtos 状态),会为以后字节码生成 9 个入口地址,由 bep、cep 等保留下来。如生成 aep 入口时,因为以后执行的字节码栈不须要顶缓存状态,所以要把值压入表达式栈中,而后跳转到 L 处执行,也就是相当于从 vep 入口进入执行了。
当初简略梳理一下,上一个字节码指令到底从哪个入口进入到下一个字节码指令要通过上一个字节码指令的执行后果而定。如果上一个字节码指令执行的后果为 fep,而以后字节码指令执行之前的栈顶缓存状态要求是 vtos,则从 TemplateInterpreterGenerator::set_vtos_entry_points()函数中给 fep 赋值的中央开始执行。所以说,上一个字节码指令的执行后果和下一个将要执行的字节码指令执行之前要求的栈顶缓存状态独特决定了从哪个入口进入。
push_f()函数的实现如下:
源代码地位:/hotspot/src/cpu/x86/vm/interp_masm_x86_64.cpp
void InterpreterMacroAssembler::push_f(XMMRegister r) { // r 的默认值为 xmm0
subptr(rsp, wordSize); // wordSize 为机器字长,64 位下为 8 字节,所以值为 8
movflt(Address(rsp, 0), r);
}
void MacroAssembler::subptr(Register dst, int32_t imm32) {LP64_ONLY(subq(dst, imm32)) NOT_LP64(subl(dst, imm32));
}
void Assembler::subq(Register dst, int32_t imm32) {(void) prefixq_and_encode(dst->encoding());
emit_arith(0x81, 0xE8, dst, imm32);
}
void Assembler::emit_arith(int op1, int op2, Register dst, int32_t imm32) {assert(isByte(op1) && isByte(op2), "wrong opcode");
assert((op1 & 0x01) == 1, "should be 32bit operation");
assert((op1 & 0x02) == 0, "sign-extension bit should not be set");
if (is8bit(imm32)) {emit_int8(op1 | 0x02); // set sign bit
emit_int8(op2 | encode(dst));
emit_int8(imm32 & 0xFF);
} else {emit_int8(op1);
emit_int8(op2 | encode(dst));
emit_int32(imm32);
}
}
调用 emit_arith()、emit_int8()等函数生成机器指令片段,生成的内容最初会存储到 StubQueue 的 InterpreterCodelet 实例中,对于机器指令和生成存储过程在之前曾经介绍过,这里不做过多介绍。
set_vtos_entry_points()函数生成的机器指令片段通过反编译后,对应的汇编代码后如下:
// aep 的入口
push %rax
jmpq L
// fep 入口
sub $0x8,%rsp
movss %xmm0,(%rsp)
jmpq L
// dep 入口
sub $0x10,%rsp
movsd %xmm0,(%rsp)
jmpq L
// lep 入口
sub $0x10,%rsp
mov %rax,(%rsp)
jmpq L
// iep 入口
push %rax
// ---- L ----
set_vtos_entry_points()函数最初调用 generate_and_dispatch()函数写入以后字节码指令对应的机器指令片段和跳转到下一个字节码指令继续执行的逻辑解决局部。
generate_and_dispatch()函数的次要实现如下:
void TemplateInterpreterGenerator::generate_and_dispatch(Template* t, TosState tos_out) {
// 生成以后字节码指令对应的机器指令片段
t->generate(_masm);
if (t->does_dispatch()) {// asserts} else {
// 生成散发到下一个字节码指令的逻辑
__ dispatch_epilog(tos_out, step);
}
}
这里以 iconst 字节码为例剖析 generate()函数的实现:
void Template::generate(InterpreterMacroAssembler* masm) {
// parameter passing
TemplateTable::_desc = this;
TemplateTable::_masm = masm;
// code generation
_gen(_arg);
masm->flush();}
generate()函数会调用生成器函数_gen(_arg),对于 iconst 指令来说,生成器函数为 iconst()。generate()函数依据平台而不同,如 x86_64 平台下,定义如下:
源代码地位:/hotspot/src/cpu/x86/vm/templateTable_x86_64.cpp
void TemplateTable::iconst(int value) {if (value == 0) {__ xorl(rax, rax);
} else {__ movl(rax, value);
}
}
咱们晓得,iconst_i 指令是将 i 压入栈,这里生成器函数 iconst()在 i 为 0 时,没有间接将 0 写入 rax,而是应用异或运算清零,即向代码缓冲区写入指令”xor %rax, %rax”;当 i 不为 0 时,写入指令”mov $0xi, %rax”
当不须要转发时,会在 TemplateInterpreterGenerator::generate_and_dispatch()函数中调用 dispatch_epilog()函数生成取下一条指令和分派的指标代码:
void InterpreterMacroAssembler::dispatch_epilog(TosState state, int step) {dispatch_next(state, step);
}
dispatch_next()函数的实现如下:
void InterpreterMacroAssembler::dispatch_next(TosState state, int step) {// load next bytecode (load before advancing r13 to prevent AGI)
load_unsigned_byte(rbx, Address(r13, step));
// advance r13
increment(r13, step);
dispatch_base(state, Interpreter::dispatch_table(state));
}
这个函数在之前曾经介绍过,这里不再介绍。
第 15 章 - 解释器及解释器生成器
办法解释执行时须要解释器与解释器生成器的反对。解释器与解释器生成器的继承体系如下:
上面具体介绍解释器与解释器生成器。
1、解释器
解释器是一堆本地代码例程结构的,这些例程会在虚拟机启动的时候写入到 StubQueue 中,当前解释执行时就只须要进入指定例程即可。
解释器的继承体系如下:
AbstractInterpreter /interpreter/abstractInterpreter.hpp
CppInterpreter
TemplateInterpreter /interpreter/templateInterpreter.hpp
Interpreter /interpreter/templateInterpreter.hpp
Interpreter 通过宏能够继承自 CppInterpreter 或者 TemplateInterpreter,前者称为 C ++ 解释器,每个字节码指令都对应一段 C ++ 代码,通过 switch 的形式解决字节码,后者称为模板解释器,每个指令对应一段机器指令片段,通过指令模板的形式解决字节码,HotSpot VM 默认应用模板解释器。
(1)形象解释器 AbstractInterpreter
所有的解释器都继承自形象解释器,类及重要属性的定义如下:
class AbstractInterpreter{
StubQueue* _code
address _entry_table[n];
// ...
};
_code 属性在之前曾经介绍过,这是一个队列,队列中的 InterpreterCodelet 示意一个例程,比方 iconst_1 对应的代码,invokedynamic 对应的代码,异样解决对应的代码,办法入口点对应的代码,这些代码都是一个个 InterpreterCodelet。整个解释器都是由这些例程组成的,每个例程实现解释器的局部性能,以此实现整个解释器。
_entry_table 数组会会保留办法入口点,例如一般办法的入口点为_entry_table[0]、同步的一般办法的入口点为_entry_table[1],这些_entry_table[0],_entry_table[1]指向的就是之前_code 队列外面的例程。这些逻辑都在是 generate_all()函数中实现的,如下:
void TemplateInterpreterGenerator::generate_all() {
// ...
method_entry(zerolocals)
method_entry(zerolocals_synchronized)
method_entry(empty)
method_entry(accessor)
method_entry(abstract)
method_entry(java_lang_math_sin)
method_entry(java_lang_math_cos)
method_entry(java_lang_math_tan)
method_entry(java_lang_math_abs)
method_entry(java_lang_math_sqrt)
method_entry(java_lang_math_log)
method_entry(java_lang_math_log10)
method_entry(java_lang_math_exp)
method_entry(java_lang_math_pow)
method_entry(java_lang_ref_reference_get)
// ...
}
method_entry 宏的定义如下:
#define method_entry(kind) \
{ \
CodeletMark cm(_masm, "method entry point (kind =" #kind ")"); \
Interpreter::_entry_table[Interpreter::kind] = generate_method_entry(Interpreter::kind); \
}
能够看到,调用 generate_method_entry()函数会返回例程对应的入口地址,而后保留到 AbstractInterpreter 类中定义的_entry_table 数组中。调用 generate_method_entry()函数传入的参数是枚举常量,示意一些非凡的办法和一些常见的办法类型。
(2)模板解释器 TemplateInterpreter
模板解释器类的定义如下:
class TemplateInterpreter: public AbstractInterpreter {
protected:
// 数组越界异样例程
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;
// ...
}
形象解释器定义了必要的例程,具体的解释器在这之上还有本人的特设的例程。模板解释器就是一个例子,它继承自形象解释器,在那些例程之上还有本人的特设例程,例如下面定义的一些属性,保留了程序异样时的入口例程,其实还有许多为保留例程入口而定义的字段或数组,这里就不一一介绍了。
(3)解释器 Interpreter
类的定义如下:
class Interpreter: public CC_INTERP_ONLY(CppInterpreter) NOT_CC_INTERP(TemplateInterpreter) {// ...}
没有定义新的属性,只有几个函数。Interpreter 默认通过宏扩大的形式继承 TemplateInterpreter。
2、解释器生成器
要想得到可运行的解释器还须要解释器生成器。解释器生成器原本能够单独实现填充工作,可能为理解耦,也可能是为了构造清晰,HotSpot VM 将字节码的例程抽了进去放到了 TemplateTable 模板表中,它辅助模板解释器生成器 templateInterpreterGenerator 生成各种例程。
解释器生成器的继承体系如下:
AbstractInterpreterGenerator /interpreter/abstractInterpreter.hpp
TemplateInterpreterGenerator /interpreter/templateInterpreter.hpp
InterpreterGenerator /interpreter/interpreter.hpp
模板解释器生成器扩大了形象解释器生成器。解释器生成器与解释器其实有某种意义上的对应关系,如形象解释器生成器中定义了一些函数,调用这些函数会初始化形象解释器中的属性,如保留例程的_entry_table 数组等,在模板解释器生成器中定义的函数会初始化模板解释器中定义的一些属性,如_throw_ArrayIndexOutOfBoundsException_entry 等。之前介绍过空指针的例程就是在这个 TemplateInterpreterGenerator 类的 generate_all()函数中生成的。如下:
{CodeletMark cm(_masm, "throw exception entrypoints");
// ...
Interpreter::_throw_NullPointerException_entry = generate_exception_handler("java/lang/NullPointerException",NULL);
// ...
}
对于解释器生成器不再过多介绍。
这里咱们须要揭示的是,解释器和解释器生成器中定义的函数在实现过程中,有些和平台是无关的,所以会在 /interpreter 文件夹下的文件中实现。例如 Interpreter 和 InterpreterGenerator 类定义在 /interpreter 文件夹下,其中定义的函数会在 /interpreter 文件夹下的 interpreter.cpp 文件中实现,然而有些函数是针对特定平台,咱们只探讨 linux 在 x86 架构下的 64 位实现,所以 cpu/x86/vm 文件夹下也有 interpreter_x86.hpp 和 interpreter_x86_64.cpp 等文件,只须要在定义 Interpreter 类时蕴含 interpreter_x86.hpp 文件即可。
第 16 章 - 虚拟机中的汇编器
汇编器的继承体系如下:
为解析器提供的相干汇编接口,所以每个字节码指令都会关联一个生成器函数,而生成器函数会调用汇编器生成机器指令片段,例如为 iload 字节码指令生成例程时,调用的生成函数为 TemplateTable::iload(int n),此函数的实现如下:
源代码地位:hotspot/src/cpu/x86/vm/templateTable_x86_64.cpp
void TemplateTable::iload() {transition(vtos, itos);
...
// Get the local value into tos
locals_index(rbx);
__ movl(rax, iaddress(rbx)); // iaddress(rb)为源操作数,rax 为目地操作数
}
函数调用了__ movl(rax,iaddress(rbx))函数生成对应的机器指令,所生成的机器指令为 mov reg,operand,其中__为宏,定义如下:
源代码地位:hotspot/src/cpu/x86/vm/templateTable_x86_64.cpp
#define __ _masm->
_masm 变量的定义如下:
源代码地位:hotspot/src/share/vm/interpreter/abstractInterpreter.hpp
class AbstractInterpreterGenerator: public StackObj {
protected:
InterpreterMacroAssembler* _masm;
...
}
最终_masm 会被实例化为 InterprterMacroAssembler 类型。
在 x86 的 64 位平台上调用 movl()函数的实现如下:
源代码地位:hotspot/src/cpu/x86/vm/assembler_86.cpp
void Assembler::movl(Register dst, Address src) {InstructionMark im(this);
prefix(src, dst);
emit_int8((unsigned char)0x8B);
emit_operand(dst, src);
}
调用 prefix()、emit_int8()等定义在汇编器中的函数时,这些函数会通过 AbstractAssembler::_code_section 属性向 InterpreterCodelet 实例中写入机器指令,这个内容在之前曾经介绍过,这里不再介绍。
1、AbstractAssembler 类
AbstractAssembler 类中定义了生成汇编代码的形象公共根底函数,如获取关联 CodeBuffer 的以后内存地位的 pc()函数,将机器指令全副刷新到 InterpreterCodelet 实例中的 flush()函数,绑定跳转标签的 bind()函数等。AbstractAssembler 类的定义如下:
源代码地位:hotspot/src/share/vm/asm/assembler.hpp
// The Abstract Assembler: Pure assembler doing NO optimizations on the
// instruction level; i.e., what you write is what you get.
// The Assembler is generating code into a CodeBuffer.
class AbstractAssembler : public ResourceObj {
friend class Label;
protected:
CodeSection* _code_section; // section within the code buffer
OopRecorder* _oop_recorder; // support for relocInfo::oop_type
...
void emit_int8(int8_t x) {code_section()->emit_int8(x);
}
void emit_int16(int16_t x) {code_section()->emit_int16(x);
}
void emit_int32(int32_t x) {code_section()->emit_int32(x);
}
void emit_int64(int64_t x) {code_section()->emit_int64(x);
}
void emit_float(jfloat x) {code_section()->emit_float(x);
}
void emit_double(jdouble x) {code_section()->emit_double(x);
}
void emit_address(address x) {code_section()->emit_address(x);
}
...
}
汇编器会生成机器指令序列,并且将生成的指令序列存储到缓存中,而_code_begin 指向缓存区首地址,_code_pos 指向缓存区的以后可写入的地位。
这个汇编器提供了写机器指令的根底函数,通过这些函数可不便地写入 8 位、16 位、32 位和 64 位等的数据或指令。这个汇编器中解决的业务不会依赖于特定平台。
2、Assembler 类
assembler.hpp 文件中除定义 AbstractAssembler 类外,还定义了 jmp 跳转指令用到的标签 Lable 类,调用 bind()函数后就会将以后 Lable 实例绑定到指令流中一个特定的地位,比方 jmp 指令接管 Lable 参数,就会跳转到对应的地位处开始执行,可用于实现循环或者条件判断等控制流操作。
Assembler 的定义跟 CPU 架构无关,通过 assembler.hpp 文件中的宏蕴含特定 CPU 下的 Assembler 实现,如下:
源代码地位:hotspot/src/share/vm/asm/assembler.hpp
#ifdef TARGET_ARCH_x86
# include "assembler_x86.hpp"
#endif
Assembler 类增加了特定于 CPU 架构的指令实现和指令操作相干的枚举。定义如下:
源代码地位:hotspot/src/cpu/x86/vm/assembler_x86.hpp
// The Intel x86/Amd64 Assembler: Pure assembler doing NO optimizations on the instruction
// level (e.g. mov rax, 0 is not translated into xor rax, rax!); i.e., what you write
// is what you get. The Assembler is generating code into a CodeBuffer.
class Assembler : public AbstractAssembler {...}
提供的许多函数根本是对单个机器指令的实现,例如某个 movl()函数的实现如下:
void Assembler::movl(Register dst, Address src) {InstructionMark im(this);
prefix(src, dst);
emit_int8((unsigned char)0x8B);
emit_operand(dst, src);
}
subq()函数的实现如下:
void Assembler::subq(Register dst, int32_t imm32) {(void) prefixq_and_encode(dst->encoding());
emit_arith(0x81, 0xE8, dst, imm32);
}
如上函数将会调用 emit_arith()函数,如下:
void Assembler::emit_arith(int op1, int op2, Register dst, int32_t imm32) {assert(isByte(op1) && isByte(op2), "wrong opcode");
assert((op1 & 0x01) == 1, "should be 32bit operation");
assert((op1 & 0x02) == 0, "sign-extension bit should not be set");
if (is8bit(imm32)) {emit_int8(op1 | 0x02); // set sign bit
emit_int8(op2 | encode(dst));
emit_int8(imm32 & 0xFF);
} else {emit_int8(op1);
emit_int8(op2 | encode(dst));
emit_int32(imm32);
}
}
调用 emit_int8()或 emit_int32()等函数写入机器指令。最初写入的指令如下:
83 EC 08
因为 8 可由 8 位有符号数示意,第一个字节为 0x81 | 0x02,即 0x83,rsp 的寄存器号为 4,第二个字节为 0xE8 | 0x04,即 0xEC,第三个字节为 0x08 & 0xFF,即 0x08,该指令即 AT&T 格调的 sub $0x8,%rsp。
我在这里并不会具体解读汇编器中 emit_arith()等函数的实现逻辑,这些函数如果在不了解机器指令编码的状况下很难了解其实现过程。前面咱们依据 Intel 手册介绍了机器指令编码格局后会选几个典型的实现进行解读。
3、MacroAssembler 类
MacroAssembler 类继承自 Assembler 类,次要是增加了一些罕用的汇编指令反对。类的定义如下:
源代码地位:hotspot/src/cpu/x86/vm/macroAssembler_x86.hpp
// MacroAssembler extends Assembler by frequently used macros.
//
// Instructions for which a 'better' code sequence exists depending
// on arguments should also go in here.
class MacroAssembler: public Assembler {...}
这个类中的函数通过调用 MacroAssembler 类或 Assembler 类中定义的一些函数来实现,能够看作是通过对机器指令的组合来实现一些便于业务代码操作的函数。
依据一些办法传递的参数可知,可能反对 JVM 外部数据类型级别的操作。
例如机器指令在做加法操作时,不容许两个操作数同时都是存储器操作数,或者一个来自内存,另外一个来自立刻数,然而 MacroAssembler 汇编器中却提供了这样的函数。
4、InterpreterMacroAssembler 类
在 templateTable.hpp 文件中曾经依据平台判断要引入的文件了,如下:
#ifdef TARGET_ARCH_x86
# include "interp_masm_x86.hpp"
#endif
在 interp_masm_x86.hpp 文件中定义了 InterpreterMacroAssembler 类,如下:
源代码地位:hotspot/src/cpu/x86/vm/interp_masm_x86.hpp
// This file specializes the assember with interpreter-specific macros
class InterpreterMacroAssembler: public MacroAssembler {
...
#ifdef TARGET_ARCH_MODEL_x86_64
# include "interp_masm_x86_64.hpp"
#endif
...
}
对于 64 位平台来说,引入了 interp_masm_x86_64.hpp 文件。
在 interp_masm_x86_64.cpp 文件中定义了如下几个函数:
(1)InterpreterMacroAssembler::lock_object()
(2)InterpreterMacroAssembler::unlock_object()
(3)InterpreterMacroAssembler::remove_activation()
(4)InterpreterMacroAssembler::dispatch_next()
其中的 dispatch_next()函数大家应该不生疏,这个函数在之前介绍过,是为散发字节码指令生成例程;lock_object()和 unlock_object()是在解释执行的状况下,为加载和开释锁操作生成对应的例程,在前面介绍锁相干的常识时会具体介绍;remove_activation()函数示意移除对应的栈帧,例如在遇到异样时,如果以后的办法不能解决此异样,那就须要对栈进行破坏性开展,在开展过程中须要移除对应的栈帧。
公众号【深刻分析 Java 虚拟机 HotSpot】曾经更新虚拟机源代码分析相干文章到 60+,欢送关注,如果有任何问题,可加作者微信 mazhimazh,拉你入虚拟机群交换。