[讨论] HotSpot 解释器是怎样执行bytecode 的

ZHH2009 2014-07-17
能进到InterpreterRuntime::_new() 里就已经是慢速分配的场景了,
并且只会为对象分配内存空间,

但是执行构造函数的java代码是由接下来的那条invokespecial触发的。

汇编代码也可以调的,把汇编代码调顺了会知道更多细节,
比如参数是怎么传给构造函数的,new如何转到invokespecial,

invokespecial如何得到InterpreterRuntime::_new()里生成的oop,
对象的字段如何得到值?


InterpreterRuntime类中那些代码基本上是由汇编代码触发的,
要是把里面的C++代码用汇编来实现会导致汇编代码很长很难维护,所以不太实际。

看懂汇编了,才是真懂了,否则只算是了解了中间的某个过程。
小施_重名后缀 2014-07-21
我觉得楼主的这个问题应该是分为3个部分.一个是 bytecode怎么变为汇编并执行.

然后就是 _new 和 invokespecial 的执行情况.

第一个问题. 就是虚拟机在启动的时候,会给每个bytecode准备好该字节码的汇编代码.

首先是在, TemplateTable::initialize() 里,给每个字节码准备他的生成函数.

类似这样

def(Bytecodes::_iload_0             , ____|____|____|____, vtos, itos, iload               ,  0           );

 def(Bytecodes::_iconst_2            , ____|____|____|____, vtos, itos, iconst              ,  2           );


 def(Bytecodes::_imul                , ____|____|____|____, itos, itos, iop2                , mul          );



然后再 TemplateInterpreterGenerator::generate_all() -> TemplateInterpreterGenerator::set_entry_points_for_all_bytes()->
set_entry_points()->
set_short_entry_points()->  //这里可能也是wide
generate_and_dispatch()

中,会给每个字节码生成汇编代码.

其中  t->generate(_masm); 就是执行前面的def里指定的函数啦.

__ dispatch_epilog(tos_out, step); 这个就去是跳到下一个bytecode对应的汇编代码

举个例子
static int zoo(int i)
{
	i*=2;
	return	foo(i);
}




i*=2 部分;  的字节码就是

        
 
          iload_0       
          iconst_2      
          imul          
          istore_0  


从iconst_2的汇编代码是这样的:(1,不从iload_0开始是因为这里还有些细节,不过不影响解释原理.2 不同的tos,这里的代码会有点不同) 我这里是 itos.
注意,r13寄存器,存放的就是当前字节码的地址.


(gdb) x /10i $rip
=> 0x7fffed02588f:	push   %rax
   0x7fffed025890:	mov    $0x2,%eax  //eax = 2;
   0x7fffed025895:	movzbl 0x1(%r13),%ebx // ebx = r13 [1]
   0x7fffed02589a:	inc    %r13 // ++ r13;
   0x7fffed02589d:	movabs $0x7ffff7018ea0,%r10
   0x7fffed0258a7:	jmpq   *(%r10,%rbx,8)



进入这段代码的时候
(gdb) p $ebx
$17 = 5   //iconst_2 =5


这条mov    $0x2,%eax 可以看桌是 iconst_2的本体. 后面的就是到下条字节码的过程.

执行到 0x7fffed02589a 的时候, 下一条字节码就已经到ebx了

(gdb) p $ebx
$18 = 104  // _imul = 104

其实就是跳到 0x7ffff7018ea0  + rbx * 8 , rbx = r13[1] 就是下一条字节码的值, 8 是因为我用的是64位.

0x7ffff7018ea0 魔数是怎么来的呢? 其实他就是当前tos下各个字节码数组地址了,他是一个数组,数组的每个成员都指向该字节码的汇编代码的入口.

我当前是 itos, 也就是3.
(gdb) p  &(TemplateInterpreter::_active_table._table[3])
$40 = (u_char *(*)[256]) 0x7ffff7018ea0


0x7fffed0258a7: jmpq   *(%r10,%rbx,8) 执行这一条以后,他就跳到 imul对应的汇编代码了.

(gdb) x /10i $pc
=> 0x7fffed0292c7: mov    (%rsp),%edx
   0x7fffed0292ca: add    $0x8,%rsp
   0x7fffed0292ce: imul   %edx,%eax
   0x7fffed0292d1: movzbl 0x1(%r13),%ebx
   0x7fffed0292d6: inc    %r13
   0x7fffed0292d9: movabs $0x7ffff7018ea0,%r10
   0x7fffed0292e3: jmpq   *(%r10,%rbx,8)

现在是itos,其中栈顶元素是i的值, 大家应该知道, rsp就是当前的栈. 执行过程.

mov    (%rsp),%edx // edx = i;
0x7fffed0292ca: add    $0x8,%rsp // 栈的地址加,是弹出成员

之前iconst_2的时候, 赋值 eax =2.

0x7fffed0292ce: imul   %edx,%eax  这里就是做乘法了.结果在eax里.

然后准备跳到下一句.临走前看一眼 ebx,下一个bytecode.

(gdb) p $ebx
$21 = 59   59就是 _istore_0 啦. 跳过去以后

(gdb) x /10i $pc
=> 0x7fffed027c07: mov    %eax,(%r14)
   0x7fffed027c0a: movzbl 0x1(%r13),%ebx
   0x7fffed027c0f: inc    %r13
   0x7fffed027c12: movabs $0x7ffff701b6a0,%r10
   0x7fffed027c1c: jmpq   *(%r10,%rbx,8)



说白了,就是在TemplateInterpreter::_active_table._table[tos] 里跳来跳去.


要吃饭了. _new 和 invokespecia 的执行过程我又空补上.

小施_重名后缀 2014-07-21
接上


前面已经提到字节码的汇编生成函数是在  TemplateTable::initialize() 定义的, 查看实现直接来这里看就可以了.
def(Bytecodes::_new                 , ubcp|____|clvm|____, vtos, atos, _new                ,  _           );



看 void TemplateTable::_new()这个函数.

一开始 是

__ get_unsigned_2_byte_index_at_bcp(rdx, 1); //就是取index, 也就是 
 //new           #1 的那个 1

 __ get_cpool_and_tags(rsi, rax);
//获取constant poll 和 class 的tags.


现在
rdx = class_index,
rsi = cpoll
rax=tags
还有之前提到过的 r13 = 当前bytecode地址,好像叫bcp?


__ movptr(rsi, Address(rsi, rdx,
            Address::times_8, sizeof(constantPoolOopDesc)));
//获取 instanceKlass 对象, rsi = rsi + rdx* 8 + sizeof(constantPoolOopDesc)


  __ cmpl(Address(rsi,
                  instanceKlass::init_state_offset_in_bytes() +
                  sizeof(oopDesc)),
          instanceKlass::fully_initialized);  
  __ jcc(Assembler::notEqual, slow_case); // 判断instanceKlass  是否完全初始化,没有就到慢分配.




  __ movl(rdx,
          Address(rsi,
                  Klass::layout_helper_offset_in_bytes() + sizeof(oopDesc)));


rdx = 对象长度


然后尝试去 tlb分配,如果启用的话.

查看tlb的空间够不够.
  __ movptr(rax, Address(r15_thread, in_bytes(JavaThread::tlab_top_offset())));
    __ lea(rbx, Address(rax, rdx, Address::times_1));
    __ cmpptr(rbx, Address(r15_thread, in_bytes(JavaThread::tlab_end_offset())));
    __ jcc(Assembler::above, allow_shared_alloc ? allocate_shared : slow_case);


伪代码就是.
rax = tlb_top.
rbx = tlb_top + rdx. //rdx 是对象长度

if( rbx > tlb_end)
  goto 慢分配或 shared eden



shared eden里的分配

   
    __ movptr(rax, Address(RtopAddr, 0)); //rax = top.


    __ bind(retry);
    __ lea(rbx, Address(rax, rdx, Address::times_1));//rbx = top+ 对象长度
    __ cmpptr(rbx, Address(RendAddr, 0));
    __ jcc(Assembler::above, slow_case); // 如果 top+ 对象长度 超过 end了, 就是没空间了,跳到慢分配.


    if (os::is_MP()) {
      __ lock();
    }
    __ cmpxchgptr(rbx, Address(RtopAddr, 0));  //cas的方式来设置值,被其他人修改top,说明在被其他线程在我们分配时分配了个对象,就要重试.

    // if someone beat us on the allocation, try again, otherwise continue
    __ jcc(Assembler::notEqual, retry);




后面就是把对象的那段地址设成0,还有设置对象头


   
 __ xorl(rcx, rcx); // rcx = 0
    __ shrl(rdx, LogBytesPerLong);  // divide by oopSize to simplify the loop
    {
      Label loop;
      __ bind(loop);
      __ movq(Address(rax, rdx, Address::times_8,
                      sizeof(oopDesc) - oopSize),
              rcx);// 
      __ decrementl(rdx);//
      __ jcc(Assembler::notZero, loop);  //用个循环把对象体设置成0.
    }

//后面的代码就是设置对象头



简单的说就是获取 cpool里的index,然后后去instancesKlass,获取对象长度,分配空间,对象体清零.

慢分配的方式, 调用的就是 InterpreterRuntime::_new  都是cpp代码,就不用多说了.
小施_重名后缀 2014-07-21
接上

之后就是调用构造函数, 看字节码就知道其实就是调用个普通的invokespecial

直接看  TemplateTable::invokespecial

1. prepare_invoke() 函数.获取 index,根据index去const pool里获取 metod的信息,比如名称,签名什么的,保存当前的bytecode的指针位置, 然后解析,运行时链接. 总之获取一个完全准备好的 methodoop


之后调用
2 InterpreterMacroAssembler::jump_from_interpreted
  2.1 在prepare_to_jump_from_interpreted 里. 把当前 r13 保存起来.

2.2  获取  method->interpreter_entry() 来跳 解释器方法或jit方法的入口.

在解释执行的时候.其实是调到 InterpreterGenerator::generate_normal_entry 所生成的汇编代码中的.



generate_normal_entry 里.先做检查准备, 例如 generate_stack_overflow_check 检查stack够不够

然后开始按照 _max_locals,和函数参数的个数_size_of_parameters,把本地变量入栈初始化为0.

接下来的generate_fixed_frame 来把栈帧的完整(不是平常说的操作数栈那个,是rsp,rbp这个和c++一样的东西,还有java调用约定的其他信息).另外还要从 constMethod 中把被调用函数的第一个字节码,设置到r13去.constMethod 其实就是从classFileparser解析出来的


最后调用 dispatch_next() 就是开始执行新方法的第一个bytecode对应的汇编代码了.

ZHH2009 2014-07-21
小施_重名后缀 写道

1. prepare_invoke() 函数.获取 index,根据index去const pool里获取 metod的信息,比如名称,签名什么的,保存当前的bytecode的指针位置, 然后解析,运行时链接. 总之获取一个完全准备好的 methodoop


请问这个index跟javap打印出来的那个invokespecial #XXX是否一样?
如果不一样,为什么不一样?

ConstantPoolCache的内存布局又如何理解?在这里如何修改它的值?


细节就是魔鬼,理解一个问题,需要先理解更前面的问题,
任何片断性的解释都是不够准确了。


光从HotSpot里的宏汇编是不足以理解问题的,
debug起来,左边一个memory框,中间一个真实的汇编代码框,右边一个register框,
把堆栈的变化情况一个个绘制下来,这样才有可能真正理解HotSpot里的细节。


HotSpot最好的地方不是在架构,架构设计得非常烂,
而是体现在某一些细节之处,字节码的解释执行就是其中之一,
尝试着写个java方法,
然后完整debug一遍:
call_stub -> method_entry_point -> invokeXXX -> put/getXXX -> return

完全理解了这样一条链的前前后后的所有细节,
我之前回贴中提的三个大问题就已经理解70%了,
HotSpot的解释器之所以还有实用性(有些vm根本就不提供解释器),
在我看来确实是里面的汇编代码细节实现得挺精妙的,性能不算太差。
小施_重名后缀 2014-07-23
既然cpool cache的部分遗漏了,我就把他加上吧.

首先就是要确定 cpool cache在哪里, 我们看看栈帧. (frame_x86.hpp)

// Layout of asm interpreter frame:
//    [expression stack      ] * <- sp
//    [monitors              ]   \
//     ...                                      | monitor block size
//    [monitors              ]   /
//    [monitor block size    ]
//    [byte code index/pointr]                   = bcx()                bcx_offset
//    [pointer to locals     ]                   = locals()             locals_offset
//    [constant pool cache   ]                   = cache()              cache_offset
//    [methodData            ]                   = mdp()                mdx_offset
//    [methodOop             ]                   = method()             method_offset
//    [last sp               ]                   = last_sp()            last_sp_offset
//    [old stack pointer     ]                     (sender_sp)          sender_sp_offset
//    [old frame pointer     ]   <- fp           = link()  
//    [return pc             ]
//    [oop temp              ]                     (only for native calls)
//    [locals and parameters ]
//                               <- sender sp


所谓的fp就是 rbp, sp就是 rsp,在图上向上方向,栈增加,地址减小.

看这个函数 resolve_cache_and_index --> get_cache_and_index_at_bcp

首先要取得方法的index,紧跟在invoke后面. 也就是r13,

引用
load_unsigned_short(index, Address(r13, bcp_offset));//

即: index = *(r13+1);


之后就是获取cpool cache. 从栈帧可以看到他在rbp上面5个位置

movptr(cache, Address(rbp, frame::interpreter_frame_cache_offset * wordSize));  

cache = *( rbp + 5*8);

//后面有代码.

shll(index, 2);  index = index *4 . //为啥要莫名的乘以4呢?



出来以后

movl(temp, Address(Rcache, index, Address::times_ptr,  constantPoolCacheOopDesc::base_offset() + ConstantPoolCacheEntry::indices_offset()));

这个寻址稍微复杂一点.

直接写就是   temp = cache + index * 8 + sizeof(constantPoolCacheOopDesc) + (ConstantPoolCacheEntry*)0 -> _indices



(ConstantPoolCacheEntry*)0 -> _indices 就是 _indices 在 ConstantPoolCacheEntry 中的偏移量.


回想一下上面那个被莫名的乘以4. 整个表达式其实应该写成

cache + sizeof(constantPoolCacheOopDesc) +  原始的index * 32 + _indices 在 ConstantPoolCacheEntry 中的偏移量.

而 sizeof(ConstantPoolCacheEntry ) 正好就是32.

如果对内存布局比较敏感,基本上就知道是怎么回事了.

cache + sizeof(constantPoolCacheOopDesc) 定位到  constantPoolCacheOopDesc的后面的地址.

原始的index * sizeof(ConstantPoolCacheEntry ) 就是数组的成员的地址了. 再加上 _indices 的偏移量.即可获取 invoke的函数的对应的 _indices的地址


rbp + 5*8 指向的内存区域,应该是这样


[constantPoolCacheOopDesc] [ConstantPoolCacheEntry][ConstantPoolCacheEntry][ConstantPoolCacheEntry][ConstantPoolCacheEntry]

constantPoolCacheOopDesc的 _length字段,就是说后面跟着多少个entry. 由此可知, 读出来原始的index,应该是从0开始的.而个数就和javap出来的最前面的methodref, fieldref的数量

如果改成c代码,大概就是

constantPoolCacheOopDesc *cache = (constantPoolCacheOopDesc *)( *(long*)( rbp + 5*8));

ConstantPoolCacheEntry *entryArray =  (ConstantPoolCacheEntry *)((char*)cache + sizeof(constantPoolCacheOopDesc))

tmp = entryArray[raw_index]._indices


后面的代码 就查看标志位,看看是不是已经被解析过了.

引用
__ shrl(temp, shift_count);
    // have we resolved this bytecode?
    __ andl(temp, 0xFF);
    __ cmpl(temp, (int) bytecode());
    __ jcc(Assembler::equal, resolved);


另外_indices在没解析过的时候,和javap出来的#1 #2是相等的,是从1开始.

ConstantPoolCacheEntry 各个值得详细意义,可以直接查看cpCacheOop.hpp,那上面的注释也比较详细了.


解析的过程在, 入口是 InterpreterRuntime::resolve_invoke, cpp代码,细节就不用提了,随便看看应该就知道个大概了.

resolve完成以后,最后会调用

cache_entry(thread)->set_method(
      bytecode,
      info.resolved_method(),
      info.vtable_index())


给 ConstantPoolCacheEntry 设置值, 和之前略有不同的就是他通过JavaThread 来定位CacheEntry, 在JavaThread 的lastframe 和前面rbp的功能差不多,过程代码是 method(thread)->constants()->cache()->entry_at(i)
douyu 2014-07-23
楼上的兄弟,你真有把实际生成的汇编代码调试起来吗?

首先,你上面提的那个index = *(r13+1)根本就不是javap打印出来的那个invokespecial中的#XXX,
这个index是在Rewriter阶段重写过的, 
ConstantPoolCacheEntry:_indices字段的格式是
// bit number |31                0|
// bit length |-8--|-8--|---16----|
// --------------------------------
// _indices   [ b2 | b1 |  index  ]


里面最后16bit的index才是真的#XXX。


其次,上面连续的这5条汇编mov、shr、and、cmp、je也不是与标志位(_flags字段)相关的,
而是取出ConstantPoolCacheEntry::_indices字段中的b1部分,如果b1的值刚好等于invokespecial,就说明解析过了。

最后,为啥要莫名的乘以4呢? 也不是像你解释的那么复杂,甚至是错误的。

在我的32位系统上面实际的汇编是
shl    $0x2,%edx
mov    0x8(%ecx,%edx,4),%ebx //这里的4代表ConstantPoolCacheEntry每个字段的字节数
shr    $0x10,%ebx
and    $0xff,%ebx

cmp    $0xb7,%ebx
je     0x01cc5897


乘以4,是因为每个ConstantPoolCacheEntry刚好有4个字段,每个字段占用的字节数刚好又一样,都是4,
加8是因为ConstantPoolCache类的_length和_constant_pool占了8个字节,

所以对于第0个ConstantPoolCacheEntry::_indices字段的地址就是:
ConstantPoolCache的地址 + 8 + (0*4)*4,
其实就是: ConstantPoolCache的地址 + 8

第1个ConstantPoolCacheEntry::_indices字段的地址就是:
ConstantPoolCache的地址 + 8 + (1*4)*4
(这里的1*4就是上面的shl $0x2,%edx,因为多了前面的第0个ConstantPoolCacheEntry)


第2个ConstantPoolCacheEntry::_indices字段的地址就是:
ConstantPoolCache的地址 + 8 + (2*4)*4
(这里的2*4是因为多了前面的第0、1个ConstantPoolCacheEntry)

依此类推……

所以_indices字段的地址计算公式就是:

第i个_indices字段的地址 = ConstantPoolCache的地址 + 8 + (i * 4) * 4
(其中i>=0,第一个4代表ConstantPoolCacheEntry有4个字段,第二个4代表ConstantPoolCacheEntry每个字段都占用4字节)

invokespecial的汇编代码在后面还有
mov    0xc(%ecx,%edx,4),%ebx //ConstantPoolCacheEntry::_f1字段(其实是method指针)
mov    0x14(%ecx,%edx,4),%edx //ConstantPoolCacheEntry::_flags字段

0xc是因为_f1字段在_indices字段后面,偏移多了4个字节,
0x14是因为_flags字段在_indices字段后面,偏移多了12个字节。

douyu 2014-07-23
这是ConstantPoolCache的内存布局
可以在我的OpenJDK-Research上面找到: https://github.com/codefollower/OpenJDK-Research/blob/master/hotspot/my-docs/oops/ConstantPoolCache.java

/*
    偏移(10) 偏移(16)  字段                   类型
    ----     ------    --------        --------------------
    0        0         _length                int
    4        4         _constant_pool         ConstantPool *
    
    ConstantPoolCacheEntry (0)
    --------------------------
    8        8         _indices               intx //占4个字节
    12       C         _f1                    Metadata*
    16       10        _f2                    intx
    20       14        _flags                 intx
    --------------------------

    ConstantPoolCacheEntry (1)
    --------------------------
    24       18        _indices               intx //占4个字节
    28       1C        _f1                    Metadata*
    32       20        _f2                    intx
    36       24        _flags                 intx
    --------------------------

    ......

    ConstantPoolCacheEntry (n)
    --------------------------
    ......
    --------------------------
*/


根据上一个回复中的公式算一下验证一下就懂了。
douyu 2014-07-23
还是那句话,要了解所有细节,不要只光看原始的宏汇编,把实际生成的汇编代码debug起来,实际生成的汇编代码有时比原始的宏汇编简单得多。


通过上面这个invokespecial对应的汇编代码的例子就能看出HotSpot的一些细节之美,
做了相当多的优化,包括ConstantPoolCacheEntry中各类字段的使用。

当然,也有缺点:就是代码更难懂了。

比如:ClassFileParser::layout_fields就是个极端例子,
为了重排字段的布局搞了一个500多行的方法,繁琐之极。
小施_重名后缀 2014-07-24
我这个是用64位的代码,  不同的原因就是因为 wordsize 不同啊,怎么会有问题.那几个字段都是8字节的,没什么问题.

那个4实际上是字段个数.当然这不重要, 而是这个shl的指令,和后面的wordsize乘起来等于sizeof(cpcache entry)


"标志位"这个词可能用的不太合适,因为有个叫flag的字段.不过我前面的

tmp = entryArray[raw_index]._indices  应该说的很明显了吧.后面的都是比较tmp的高位.


而且关于javap的#编号,我说的是和未解析过的_indices字段相等,没说是 R13+1后面的内容.

对于 r13+1,我说的是从0开始



invoke static 对应的汇编

(gdb) x /20i $rip
=> 0x7fffed03a5cf:      push   %rax
   0x7fffed03a5d0:      mov    %r13,-0x38(%rbp)
   0x7fffed03a5d4:      movzwl 0x1(%r13),%edx
   0x7fffed03a5d9:      mov    -0x28(%rbp),%rcx 
   0x7fffed03a5dd:      shl    $0x2,%edx
   0x7fffed03a5e0:      mov    0x20(%rcx,%rdx,8),%ebx
   0x7fffed03a5e4:      shr    $0x10,%ebx
   0x7fffed03a5e7:      and    $0xff,%ebx
   0x7fffed03a5ed:      cmp    $0xb8,%ebx
   0x7fffed03a5f3:      je     0x7fffed03a84d
   0x7fffed03a5f9:      mov    $0xb8,%ebx
   0x7fffed03a5fe:      callq  0x7fffed03a608
   0x7fffed03a603:      jmpq   0x7fffed03a841
   0x7fffed03a608:      mov    %rbx,%rsi
   0x7fffed03a60b:      lea    0x8(%rsp),%rax
   0x7fffed03a610:      mov    %r13,-0x38(%rbp)
   0x7fffed03a614:      cmpq   $0x0,-0x10(%rbp)
   0x7fffed03a61c:      je     0x7fffed03a699
   0x7fffed03a622:      mov    %rsp,-0x28(%rsp)
   0x7fffed03a627:      sub    $0x80,%rsp




mov    -0x28(%rbp),%rcx   cpool cache在 rbp - 40

shl    $0x2,%edx 乘以4

(gdb) p sizeof(constantPoolCacheOopDesc)
$10 = 32

_indices是第一个字段, offset是0  32+0还是32

mov    0x20(%rcx,%rdx,8),%ebx
cpcache + 乘过4的index值  * 8 + 32

(gdb) p sizeof(ConstantPoolCacheEntry)  
$15 = 32
就是4乘以8


(gdb) p (constantPoolCacheOopDesc*)$rcx
$11 = (constantPoolCacheOopDesc *) 0xdba984f8
(gdb) p *$11
$12 = {<oopDesc> = {_mark = 0x1, _metadata = {_klass = 0xdb801900, _compressed_klass = 3682605312}, static _bs = 0x7ffff0030cc8}, _length = 6,
  _constant_pool = 0xdba97bc0}


这内存结果解释应该没问题

引用
(gdb) p *(ConstantPoolCacheEntry*)($rcx + sizeof(constantPoolCacheOopDesc))@6
$13 = {{_indices = 1, _f1 = 0x0, _f2 = 0, _flags = 0}, {_indices = 2, _f1 = 0x0, _f2 = 0, _flags = 0}, {_indices = 12058627, _f1 = 0xdba98040, _f2 = 0,
    _flags = 813694976}, {_indices = 4, _f1 = 0x0, _f2 = 0, _flags = 0}, {_indices = 5, _f1 = 0x0, _f2 = 0, _flags = 0}, {_indices = 6, _f1 = 0xf58fa990, _f2 = 112,
    _flags = 838860800}}




解释器这玩意,,跑一跑固然有不少帮助,但是看看宏汇编就能看个大概

对于这个主题,我懂得也不多,以后就不回复了.
Global site tag (gtag.js) - Google Analytics