invokevirtual字节码指令的模板定义如下:

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

生成函数为invokevirtual,传递的参数为f2_byte,也就是2,如果为2时,ConstantPoolCacheEntry::indices中取[b2,b1,index]的b2。调用的TemplateTable::invokevirtual()函数的实现如下:

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

先调用prepare_invoke()函数,后调用invokevirtual_helper()函数来生成invokevirtual字节码指令对应的汇编代码。

1、TemplateTable::prepare_invoke()函数

调用TemplateTable::prepare_invoke()函数生成的汇编代码比较多,所以我们分三部分进行查看。

第1部分:

0x00007fffe1021f90: mov    %r13,-0x38(%rbp)    // 将bcp保存到栈中
// invokevirtual x中取出x,也就是常量池索引存储到%edx,
// 其实这里已经是ConstantPoolCacheEntry的index,因为在类的连接
// 阶段会对方法中特定的一些字节码指令进行重写
0x00007fffe1021f94: movzwl 0x1(%r13),%edx
// 将ConstantPoolCache的首地址存储到%rcx 0x00007fffe1021f99: mov -0x28(%rbp),%rcx // 左移2位,因为%edx中存储的是ConstantPoolCacheEntry索引,左移2位是因为
// ConstantPoolCacheEntry占用4个字
0x00007fffe1021f9d: shl $0x2,%edx // 计算%rcx+%rdx*8+0x10,获取ConstantPoolCacheEntry[_indices,_f1,_f2,_flags]中的_indices
// 因为ConstantPoolCache的大小为0x16字节,%rcx+0x10定位
// 到第一个ConstantPoolCacheEntry的位置
// %rdx*8算出来的是相对于第一个ConstantPoolCacheEntry的字节偏移
0x00007fffe1021fa0: mov 0x10(%rcx,%rdx,8),%ebx // 获取ConstantPoolCacheEntry中indices[b2,b1,constant pool index]中的b2
0x00007fffe1021fa4: shr $0x18,%ebx // 取出indices中含有的b2,即bytecode存储到%ebx中
0x00007fffe1021fa7: and $0xff,%ebx // 查看182的bytecode是否已经连接
0x00007fffe1021fad: cmp $0xb6,%ebx // 如果连接就进行跳转,跳转到resolved
0x00007fffe1021fb3: je 0x00007fffe1022052

主要查看字节码是否已经连接,如果没有连接则需要连接,如果已经进行了连接,则跳转到resolved直接执行方法调用操作。

第2部分:

// 调用InterpreterRuntime::resolve_invoke()函数,因为指令还没有连接
// 将bytecode为182的指令移动到%ebx中
0x00007fffe1021fb9: mov $0xb6,%ebx // 通过调用MacroAssembler::call_VM()函数来调用
// InterpreterRuntime::resolve_invoke(JavaThread* thread, Bytecodes::Code bytecode)函数
// 进行方法连接
0x00007fffe1021fbe: callq 0x00007fffe1021fc8
0x00007fffe1021fc3: jmpq 0x00007fffe1022046 // 跳转到----E----
// 准备第2个参数,也就是bytecode
0x00007fffe1021fc8: mov %rbx,%rsi
0x00007fffe1021fcb: lea 0x8(%rsp),%rax
0x00007fffe1021fd0: mov %r13,-0x38(%rbp)
0x00007fffe1021fd4: mov %r15,%rdi
0x00007fffe1021fd7: mov %rbp,0x200(%r15)
0x00007fffe1021fde: mov %rax,0x1f0(%r15)
0x00007fffe1021fe5: test $0xf,%esp
0x00007fffe1021feb: je 0x00007fffe1022003
0x00007fffe1021ff1: sub $0x8,%rsp
0x00007fffe1021ff5: callq 0x00007ffff66ac528
0x00007fffe1021ffa: add $0x8,%rsp
0x00007fffe1021ffe: jmpq 0x00007fffe1022008
0x00007fffe1022003: callq 0x00007ffff66ac528
0x00007fffe1022008: movabs $0x0,%r10
0x00007fffe1022012: mov %r10,0x1f0(%r15)
0x00007fffe1022019: movabs $0x0,%r10
0x00007fffe1022023: mov %r10,0x200(%r15)
0x00007fffe102202a: cmpq $0x0,0x8(%r15)
0x00007fffe1022032: je 0x00007fffe102203d
0x00007fffe1022038: jmpq 0x00007fffe1000420
0x00007fffe102203d: mov -0x38(%rbp),%r13
0x00007fffe1022041: mov -0x30(%rbp),%r14
0x00007fffe1022045: retq
// 结束MacroAssembler::call_VM()函数的调用 // **** E ****
// 将invokevirtual x中的x加载到%edx中,也就是ConstantPoolCacheEntry的索引
0x00007fffe1022046: movzwl 0x1(%r13),%edx // 将ConstantPoolCache的首地址存储到%rcx中
0x00007fffe102204b: mov -0x28(%rbp),%rcx // %edx中存储的是ConstantPoolCacheEntry index,转换为字偏移
0x00007fffe102204f: shl $0x2,%edx

方法连接的逻辑和之前介绍的字段的连接逻辑类似,都是完善ConstantPoolCache中对应的ConstantPoolCacheEntry添加相关信息。

调用InterpreterRuntime::resolve_invoke()函数进行方法连接,这个函数的实现比较多,我们在下一篇中详细介绍。连接完成后ConstantPoolCacheEntry中的各个项如下图所示。

所以对于invokevirtual来说,通过vtable进行方法的分发,在ConstantPoolCacheEntry中,_f1字段没有使用,而对_f2字段来说,如果调用的是非final的virtual方法,则保存的是目标方法在vtable中的索引编号,如果是virtual final方法,则_f2字段直接指向目标方法的Method实例。

第3部分:

// **** resolved ****

// resolved的定义点,到这里说明invokevirtual字节码已经连接
// 获取ConstantPoolCacheEntry::_f2,这个字段只对virtual有意义
// 在计算时,因为ConstantPoolCacheEntry在ConstantPoolCache之后保存,
// 所以ConstantPoolCache为0x10,而
// _f2还要偏移0x10,这样总偏移就是0x20
// ConstantPoolCacheEntry::_f2存储到%rbx
0x00007fffe1022052: mov 0x20(%rcx,%rdx,8),%rbx
// ConstantPoolCacheEntry::_flags存储到%edx
0x00007fffe1022057: mov 0x28(%rcx,%rdx,8),%edx
// 将flags移动到ecx中
0x00007fffe102205b: mov %edx,%ecx
// 从flags中取出参数大小
0x00007fffe102205d: and $0xff,%ecx // 获取到recv,%rcx中保存的是参数大小,最终计算参数所需要的大小为%rsp+%rcx*8-0x8,
// flags中的参数大小对实例方法来说,已经包括了recv的大小
// 如调用实例方法的第一个参数是this(recv)
0x00007fffe1022063: mov -0x8(%rsp,%rcx,8),%rcx // recv保存到%rcx // 将flags存储到r13中
0x00007fffe1022068: mov %edx,%r13d
// 从flags中获取return type,也就是从_flags的高4位保存的TosState
0x00007fffe102206b: shr $0x1c,%edx // 将TemplateInterpreter::invoke_return_entry地址存储到%r10
0x00007fffe102206e: movabs $0x7ffff73b6380,%r10
// %rdx保存的是return type,计算返回地址
// 因为TemplateInterpreter::invoke_return_entry是数组,
// 所以要找到对应return type的入口地址
0x00007fffe1022078: mov (%r10,%rdx,8),%rdx
// 向栈中压入返回地址
0x00007fffe102207c: push %rdx // 还原ConstantPoolCacheEntry::_flags
0x00007fffe102207d: mov %r13d,%edx
// 还原bcp
0x00007fffe1022080: mov -0x38(%rbp),%r13

TemplateInterpreter::invoke_return_entry保存了一段例程的入口,这段例程在后面会详细介绍。

执行完如上的代码后,已经向相关的寄存器中存储了相关的值。相关的寄存器状态如下:

rbx: 存储的是ConstantPoolCacheEntry::_f2属性的值
rcx: 就是调用实例方法时的第一个参数this
rdx: 存储的是ConstantPoolCacheEntry::_flags属性的值

栈的状态如下图所示。

栈中压入了TemplateInterpreter::invoke_return_entry的返回地址。

2、TemplateTable::invokevirtual_helper()函数

调用TemplateTable::invokevirtual_helper()函数生成的代码如下:

// flags存储到%eax
0x00007fffe1022084: mov %edx,%eax
// 测试调用的方法是否为final
0x00007fffe1022086: and $0x100000,%eax
// 如果不为final就直接跳转到----notFinal----
0x00007fffe102208c: je 0x00007fffe10220c0 // 通过(%rcx)来获取receiver的值,如果%rcx为空,则会引起OS异常
0x00007fffe1022092: cmp (%rcx),%rax // 省略统计相关代码部分 // 设置调用者栈顶并保存
0x00007fffe10220b4: lea 0x8(%rsp),%r13
0x00007fffe10220b9: mov %r13,-0x10(%rbp) // 跳转到Method::_from_interpretered_entry入口去执行
0x00007fffe10220bd: jmpq *0x58(%rbx)

对于final方法来说,其实没有动态分派,所以也不需要通过vtable进行目标查找。调用时的栈如下图所示。

如下代码是通过vtable查找动态分派需要调用的方法入口 。

// **** notFinal ****

// invokevirtual指令调用的如果是非final方法,直接跳转到这里
// %rcx中存储的是receiver,用oop来表示。通过oop获取Klass
0x00007fffe10220c0: mov 0x8(%rcx),%eax // 调用MacroAssembler::decode_klass__not_null()函数生成下面的一个汇编代码
0x00007fffe10220c3: shl $0x3,%rax // LogKlassAlignmentInBytes=0x03 // 省略统计相关代码部分 // %rax中存储的是recv_klass
// %rbx中存储的是vtable_index,
// 而0x1b8为InstanceKlass::vtable_start_offset()*wordSize+vtableEntry::method_offset_in_bytes(),
// 其实就是通过动态分派找到需要调用的Method*并存储到%rbx中
0x00007fffe1022169: mov 0x1b8(%rax,%rbx,8),%rbx // 设置调用者的栈顶地址并保存
0x00007fffe1022171: lea 0x8(%rsp),%r13
0x00007fffe1022176: mov %r13,-0x10(%rbp) // 跳转到Method::_from_interpreted_entry处执行
0x00007fffe102217a: jmpq *0x58(%rbx)

理解如上代码时需要知道vtable方法分派以及vtable在InstanceKlass中的布局,这在《深入剖析Java虚拟机:源码剖析与实例详解》一书中详细介绍过,这里不再介绍。  

跳转到Method::_from_interpretered_entry保存的例程处执行,也就是以解释执行运行invokevirtual字节码指令调用的目标方法,关于Method::_from_interpretered_entry保存的例程的逻辑在第6篇、第7篇、第8篇中详细介绍过,这里不再介绍。

如上的汇编语句 mov 0x1b8(%rax,%rbx,8),%rbx 是通过调用调用lookup_virtual_method()函数生成的,此函数将vtable_entry_addr加载到%rbx中,实现如下:

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

其中的vtable_index取的就是ConstantPoolCacheEntry::_f2属性的值。

最后还要说一下,如上生成的一些汇编代码中省略了统计相关的执行逻辑,这里统计相关的代码也是非常重要的,它会辅助进行编译,所以后面我们还会介绍这些统计相关的逻辑。

参考文章:Java字节码里的invoke操作&&编译时的静态绑定与动态绑定

推荐阅读:

第1篇-关于JVM运行时,开篇说的简单些

第2篇-JVM虚拟机这样来调用Java主类的main()方法

第3篇-CallStub新栈帧的创建

第4篇-JVM终于开始调用Java主类的main()方法啦

第5篇-调用Java方法后弹出栈帧及处理返回结果

第6篇-Java方法新栈帧的创建

第7篇-为Java方法创建栈帧

第8篇-dispatch_next()函数分派字节码

第9篇-字节码指令的定义

第10篇-初始化模板表

第11篇-认识Stub与StubQueue

第12篇-认识CodeletMark

第13篇-通过InterpreterCodelet存储机器指令片段

第14篇-生成重要的例程

第15章-解释器及解释器生成器

第16章-虚拟机中的汇编器

第17章-x86-64寄存器

第18章-x86指令集之常用指令

第19篇-加载与存储指令(1)

第20篇-加载与存储指令之ldc与_fast_aldc指令(2)

第21篇-加载与存储指令之iload、_fast_iload等(3)

第22篇-虚拟机字节码之运算指令

第23篇-虚拟机字节码指令之类型转换

第24篇-虚拟机对象操作指令之getstatic

第25篇-虚拟机对象操作指令之getfield

第26篇-虚拟机对象操作指令之putstatic

第27篇-虚拟机字节码指令之操作数栈管理指令

第28篇-虚拟机字节码指令之控制转移指令

第29篇-调用Java主类的main()方法

第30篇-main()方法的执行

  

第31篇-方法调用指令之invokevirtual的更多相关文章

  1. 第33篇-方法调用指令之invokeinterface

    invokevirtual字节码指令的模板定义如下: def(Bytecodes::_invokeinterface , ubcp|disp|clvm|____, vtos, vtos, invoke ...

  2. 第35篇-方法调用指令之invokespecial与invokestatic

    这一篇将详细介绍invokespecial和invokestatic字节码指令的汇编实现逻辑 1.invokespecial指令 invokespecial指令的模板定义如下: def(Bytecod ...

  3. Spring杂谈 | 从桥接方法到JVM方法调用

    前言 之所以写这么一篇文章是因为在Spring中,经常会出现下面这种代码 // 判断是否是桥接方法,如果是的话就返回这个方法 BridgeMethodResolver.findBridgedMetho ...

  4. JAVA方法调用中的解析与分派

    JAVA方法调用中的解析与分派 本文算是<深入理解JVM>的读书笔记,参考书中的相关代码示例,从字节码指令角度看看解析与分派的区别. 方法调用,其实就是要回答一个问题:JVM在执行一个方法 ...

  5. 深入理解java虚拟机(十一) 方法调用-解析调用与分派调用

    方法调用过程是指确定被调用方法的版本(即调用哪一个方法),并不包括方法执行过程.我们知道,Class 文件的编译过程中并不包括传统编译中的连接步骤,一切方法调用在 Class 文件调用里面存储的都只是 ...

  6. 04 JVM是如何执行方法调用的(下)

    虚方法调用 Java 里所有非私有实例方法调用都会被编译成 invokevirtual 指令,而接口方法调用会被编译成 invokeinterface 指令.这两种指令,均属于 Java 虚拟机中的虚 ...

  7. jvm 字节码执行 (一)方法调用

    “虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器.硬件.指令集和操作系统层面上,而虚拟机的执行引擎是 由自己实现的,因此可以自行制定指令集 ...

  8. JVM方法调用过程

    JVM方法调用过程 重载和重写 同一个类中,如果出现多个名称相同,并且参数类型相同的方法,将无法通过编译.因此,想要在同一个类中定义名字相同的方法,那么它们的参数类型必须不同.这种方法上的联系就是重载 ...

  9. JVM系列-方法调用的原理

    JVM系列-方法调用的原理 最近重新看了一些JVM方面的笔记和资料,收获颇丰,尤其解决了长久以来心中关于JVM方法管理的一些疑问.下面介绍一下JVM中有关方法调用的知识. 目的 方法调用,目的是选择方 ...

随机推荐

  1. Django实现基本的页面分页

    1.视图views.py from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage def index(requ ...

  2. adb 常用命令大全(4)- 应用管理

    查看应用列表 语法格式 adb shell pm list packages [-f] [-d] [-e] [-s] [-3] [-i] [-u] [--user USER_ID] [FILTER] ...

  3. 快速模式第一包: quick_outI1()

    文章目录 1. 序言 2. quick_outI1()流程图 3. quick_outI1()源码分析 4. quick_outI1_continue()源码分析 5. quick_outI1_tai ...

  4. JS003. 事件监听和监听滚动条的三种参数( addEventListener( ) )

    全局 1 window.addEventListener('scroll', () => { 2 console.log('------') 3 console.log(document.doc ...

  5. 测试平台系列(55) 引入AceEditor(代码编辑器)

    大家好,我是米洛,求三连!求关注测试开发坑货! 回顾 我们上一节已经写好了左侧数据表目录,今天继续完成sql编辑器的部分. 调研组件 monaco 因为我们的项目用的是React,市面上很多编辑器都是 ...

  6. 机器学习——K-Means算法

    1 基础知识 相似度或距离 假设有 $m$ 个样本,每个样本由 $n$ 个属性的特征向量组成,样本合集 可以用矩阵 $X$ 表示 $X=[x_{ij}]_{mn}=\begin{bmatrix}x_{ ...

  7. 【C++基础教程】第五课

    上次的作业答案,非常简单. 第一题: 我们需要知道,字符(char类型)在计算机中存储的时候,是把这个字符对应的代码(专业术语叫做编码)进行存储.例如,换行符'\n'的代码就是10,'0'对应的代码就 ...

  8. 5ucms 调用当前文章的评论,以及评论列表

    <div class="center_"> <h2>共有 {field:comments} 位网友发表了评论</h2> <ul class ...

  9. 对象继承深入、call_apply、圣杯模式、构造函数和闭包,企业模块化

    一个实现加减乘除的插件:   原型其实是在构造函数之上的,构造函数变成实例化函数的时候才会有原型, 原型实际上是构造函数的一个属性 原型无非就是2个字:继承 原型中继承父类所有方法是很不合理的,因为没 ...

  10. 压测中的QPS与TPS区别

    原文来自:https://www.cnblogs.com/fkkk/p/11957566.html QPS(每秒查询率)=并发数/平均响应时间 TPS(每秒处理事务数)=请求数/时间(秒) TPS的过 ...