zoukankan      html  css  js  c++  java
  • 第32篇-解析interfacevirtual字节码指令

    在前面介绍invokevirtual指令时,如果判断出ConstantPoolCacheEntry中的_indices字段的_f2属性的值为空,则认为调用的目标方法没有连接,也就是没有向ConstantPoolCacheEntry中保存调用方法的相关信息,需要调用InterpreterRuntime::resolve_invoke()函数进行方法连接,这个函数的实现比较多,我们分几部分查看:

    InterpreterRuntime::resolve_invoke()函数第1部分:

    Handle receiver(thread, NULL);
    if (bytecode == Bytecodes::_invokevirtual || bytecode == Bytecodes::_invokeinterface) {
        ResourceMark rm(thread);
        // 调用method()函数从当前的栈帧中获取到需要执行的方法
        Method* m1 = method(thread);
        methodHandle m (thread, m1);
    
        // 调用bci()函数从当前的栈帧中获取需要执行的方法的字节码索引
        int i1 = bci(thread);
        Bytecode_invoke call(m, i1);
    
        // 当前需要执行的方法的签名
        Symbol* signature = call.signature();
    
        frame fm = thread->last_frame();
        oop x = fm.interpreter_callee_receiver(signature);
        receiver = Handle(thread,x);
    }
    

    当字节码为invokevirtual或invokeinterface这样的动态分派字节码时,执行如上的逻辑。获取到了receiver变量的值。接着看实现,如下:

    InterpreterRuntime::resolve_invoke()函数第2部分:

    CallInfo info;
    constantPoolHandle pool(thread, method(thread)->constants());
    
    {
        JvmtiHideSingleStepping jhss(thread);
        int cpcacheindex = get_index_u2_cpcache(thread, bytecode);
        LinkResolver::resolve_invoke(info, receiver, pool,cpcacheindex, bytecode, CHECK);
        ...
    } 
    
    // 如果已经向ConstantPoolCacheEntry中更新了调用的相关信息则直接返回
    if (already_resolved(thread))
      return;

    根据存储在当前栈中的bcp来获取字节码指令的操作数,这个操作数通常就是常量池缓存项索引。然后调用LinkResolver::resolve_invoke()函数进行方法连接。 这个函数会间接调用LinkResolver::resolve_invokevirtual()函数,实现如下:

    void LinkResolver::resolve_invokevirtual(
     CallInfo&           result,
     Handle              recv,
     constantPoolHandle  pool,
     int                 index,
     TRAPS
    ){
    
      KlassHandle  resolved_klass;
      Symbol*      method_name = NULL;
      Symbol*      method_signature = NULL;
      KlassHandle  current_klass;
    
      // 解析常量池时,传入的参数pool(根据当前栈中要执行的方法找到对应的常量池)和
      // index(常量池缓存项的缓存,还需要映射为原常量池索引)是有值的,根据这两个值能够
      // 解析出resolved_klass和要查找的方法名称method_name和方法签名method_signature
      resolve_pool(resolved_klass, method_name,  method_signature, current_klass, pool, index, CHECK);
    
      KlassHandle  recvrKlass(THREAD, recv.is_null() ? (Klass*)NULL : recv->klass());
    
      resolve_virtual_call(result, recv, recvrKlass, resolved_klass, method_name, method_signature, current_klass, true, true, CHECK);
    }
    

    其中会调用resolve_pool()和resolve_vritual_call()函数分别连接常量池和方法调用指令。调用会涉及到的相关函数如下图所示。

    下面介绍resolve_pool()和resolve_virtual_call()函数及其调用的相关函数的实现。

    1、LinkResolver::resolve_pool()函数

    调用的resolve_pool()函数会调用一些函数,如下图所示。

    每次调用LinkResolver::resolve_pool()函数时不一定会按如上的函数调用链执行,但是当类还没有解析时,通常会调用SystemDictionary::resolve_or_fail()函数进行解析,最终会获取到指向Klass实例的指针,最终将这个类更新到常量池中。

    resolve_pool()函数的实现如下:

    void LinkResolver::resolve_pool(
     KlassHandle& resolved_klass,
     Symbol*&     method_name,
     Symbol*&     method_signature,
     KlassHandle& current_klass,
     constantPoolHandle pool,
     int          index,
     TRAPS
    ) {
      resolve_klass(resolved_klass, pool, index, CHECK);
    
      method_name      = pool->name_ref_at(index);
      method_signature = pool->signature_ref_at(index);
      current_klass    = KlassHandle(THREAD, pool->pool_holder());
    }
    

    其中的index为常量池缓存项的索引。resolved_klass参数表示需要进行解析的类(解析是在类生成周期中连接相关的部分,所以我们之前有时候会称为连接,其实具体来说是解析的意思),而current_klass为当前拥有常量池的类,由于传递参数时是C++的引用传递,所以同值会直接改变变量的值,调用者中的值也会随着改变。

    调用resolve_klass()函数进行类解析,一般来说,类解析会在解释常量池项时就会进行,这在《深入剖析Java虚拟机:源码剖析与实例详解(基础卷)》一书中介绍过,这里需要再说一下。

    调用的resolve_klass()函数及相关函数的实现如下:

    void LinkResolver::resolve_klass(
     KlassHandle&         result,
     constantPoolHandle   pool,
     int                  index,
     TRAPS
    ) {
      Klass* result_oop = pool->klass_ref_at(index, CHECK);
      // 通过引用进行传递
      result = KlassHandle(THREAD, result_oop);
    }
    
    Klass* ConstantPool::klass_ref_at(int which, TRAPS) {
      int x = klass_ref_index_at(which);
      return klass_at(x, CHECK_NULL);
    }
    
    int klass_ref_index_at(int which) {
      return impl_klass_ref_index_at(which, false);
    }
    

    调用的impl_klass_ref_index_at()函数的实现如下:  

    int ConstantPool::impl_klass_ref_index_at(int which, bool uncached) {
      int i = which;
      if (!uncached && cache() != NULL) {
    	// 从which对应的ConstantPoolCacheEntry项中获取ConstantPoolIndex
        i = remap_instruction_operand_from_cache(which);
      }
    
      assert(tag_at(i).is_field_or_method(), "Corrupted constant pool");
      // 获取
      jint ref_index = *int_at_addr(i);
      // 获取低16位,那就是class_index
      return extract_low_short_from_int(ref_index);
    }
    

    根据断言可知,在原常量池索引的i处的项肯定为JVM_CONSTANT_Fieldref、JVM_CONSTANT_Methodref或JVM_CONSTANT_InterfaceMethodref,这几项的格式如下:

    CONSTANT_Fieldref_info{
      u1 tag;
      u2 class_index; 
      u2 name_and_type_index; // 必须是字段描述符
    }
    
    CONSTANT_InterfaceMethodref_info{
      u1 tag;
      u2 class_index; // 必须是接口
      u2 name_and_type_index; // 必须是方法描述符
    }
    
    CONSTANT_Methodref_info{
      u1 tag;
      u2 class_index; // 必须是类
      u2 name_and_type_index; // 必须是方法描述符
    }
    

    3项的格式都一样,其中的class_index索引处的项必须为CONSTANT_Class_info结构,表示一个类或接口,当前类字段或方法是这个类或接口的成员。name_and_type_index索引处必须为CONSTANT_NameAndType_info项。  

    通过调用int_at_addr()函数和extract_low_short_from_int()函数获取class_index的索引值,如果了解了常量池内存布局,这里函数的实现理解起来会很简单,这里不再介绍。

    在klass_ref_at()函数中调用klass_at()函数,此函数的实现如下:

    Klass* klass_at(int which, TRAPS) {
        constantPoolHandle h_this(THREAD, this);
        return klass_at_impl(h_this, which, CHECK_NULL);
    }

    调用的klass_at_impl()函数的实现如下:

    Klass* ConstantPool::klass_at_impl(
     constantPoolHandle this_oop,
     int                which,
     TRAPS
    ) {
      
      CPSlot entry = this_oop->slot_at(which);
      if (entry.is_resolved()) { // 已经进行了连接
        return entry.get_klass();
      }
    
      bool do_resolve = false;
      bool in_error = false;
    
      Handle  mirror_handle;
      Symbol* name = NULL;
      Handle  loader;
      {
         MonitorLockerEx ml(this_oop->lock());
    
        if (this_oop->tag_at(which).is_unresolved_klass()) {
          if (this_oop->tag_at(which).is_unresolved_klass_in_error()) {
            in_error = true;
          } else {
            do_resolve = true;
            name   = this_oop->unresolved_klass_at(which);
            loader = Handle(THREAD, this_oop->pool_holder()->class_loader());
          }
        }
      } // unlocking constantPool
    
      // 省略当in_error变量的值为true时的处理逻辑
     
      if (do_resolve) {
        oop protection_domain = this_oop->pool_holder()->protection_domain();
        Handle h_prot (THREAD, protection_domain);
        Klass* k_oop = SystemDictionary::resolve_or_fail(name, loader, h_prot, true, THREAD);
        KlassHandle k;
        if (!HAS_PENDING_EXCEPTION) {
          k = KlassHandle(THREAD, k_oop);
          mirror_handle = Handle(THREAD, k_oop->java_mirror());
        }
    
        if (HAS_PENDING_EXCEPTION) {
          ...
          return 0;
        }
    
        if (TraceClassResolution && !k()->oop_is_array()) {
          ...      
        } else {
          MonitorLockerEx ml(this_oop->lock());
          do_resolve = this_oop->tag_at(which).is_unresolved_klass();
          if (do_resolve) {
            ClassLoaderData* this_key = this_oop->pool_holder()->class_loader_data();
            this_key->record_dependency(k(), CHECK_NULL); // Can throw OOM
            this_oop->klass_at_put(which, k()); // 注意这里会更新常量池中存储的内容,这样就表示类已经解析完成,下次就不需要重复解析了
          }
        }
      }
    
      entry = this_oop->resolved_klass_at(which);
      assert(entry.is_resolved() && entry.get_klass()->is_klass(), "must be resolved at this point");
      return entry.get_klass();
    }

    函数首先调用slot_at()函数获取常量池中一个slot中存储的值,然后通过CPSlot来表示这个slot,这个slot中可能存储的值有2个,分别为指向Symbol实例(因为类名用CONSTANT_Utf8_info项表示,在虚拟机内部统一使用Symbol对象表示字符串)的指针和指向Klass实例的指针,如果类已经解释,那么指针表示的地址的最后一位为0,如果还没有被解析,那么地址的最后一位为1。

    当没有解析时,需要调用SystemDictionary::resolve_or_fail()函数获取类Klass的实例,然后更新常量池中的信息,这样下次就不用重复解析类了。最后返回指向Klass实例的指针即可。

    继续回到LinkResolver::resolve_pool()函数看接下来的执行逻辑,也就是会获取JVM_CONSTANT_Fieldref、JVM_CONSTANT_Methodref或JVM_CONSTANT_InterfaceMethodref项中的name_and_type_index,其指向的是CONSTANT_NameAndType_info项,格式如下:

    CONSTANT_NameAndType_info{
       u1 tag;
      u2 name_index;
      u2 descriptor index;
    } 
    

    获取逻辑就是先根据常量池缓存项的索引找到原常量池项的索引,然后查找到CONSTANT_NameAndType_info后,获取到方法名称和签名的索引,进而获取到被调用的目标方法的名称和签名。这些信息将在接下来调用的resolve_virtual_call()函数中使用。 

    2、LinkResolver::resolve_virtual_call()函数

    resolve_virtual_call()函数会调用的相关函数如下图所示。

    LinkResolver::resolve_virtual_call()的实现如下:

    void LinkResolver::resolve_virtual_call(
     CallInfo&     result,
     Handle        recv,
     KlassHandle   receiver_klass,
     KlassHandle   resolved_klass,
     Symbol*       method_name,
     Symbol*       method_signature,
     KlassHandle   current_klass,
     bool         check_access,
     bool         check_null_and_abstract,
     TRAPS
    ) {
      methodHandle resolved_method;
    
      linktime_resolve_virtual_method(resolved_method, resolved_klass, method_name, method_signature, current_klass, check_access, CHECK);
    
      runtime_resolve_virtual_method(result, resolved_method, resolved_klass, recv, receiver_klass, check_null_and_abstract, CHECK);
    }

    首先调用LinkResolver::linktime_resolve_virtual_method()函数,这个函数会调用如下函数:

    void LinkResolver::resolve_method(
     methodHandle&  resolved_method,
     KlassHandle    resolved_klass,
     Symbol*        method_name,
     Symbol*        method_signature,
     KlassHandle    current_klass,
     bool          check_access,
     bool          require_methodref,
     TRAPS
    ) {
    
      // 从解析的类和其父类中查找方法
      lookup_method_in_klasses(resolved_method, resolved_klass, method_name, method_signature, true, false, CHECK);
    
      // 没有在解析类的继承体系中查找到方法
      if (resolved_method.is_null()) { 
        // 从解析类实现的所有接口(包括间接实现的接口)中查找方法
        lookup_method_in_interfaces(resolved_method, resolved_klass, method_name, method_signature, CHECK);
        // ...
    
        if (resolved_method.is_null()) {
          // 没有找到对应的方法
          ...
        }
      }
    
      // ...
    } 
    

    如上函数中最主要的就是根据method_name和method_signature从resolved_klass类中找到合适的方法,如果找到就赋值给resolved_method变量。

    调用lookup_method_in_klasses()、lookup_method_in_interfaces()等函数进行方法的查找,这里暂时不介绍。

    下面接着看runtime_resolve_virtual_method()函数,这个函数的实现如下:

    void LinkResolver::runtime_resolve_virtual_method(
     CallInfo&      result,
     methodHandle   resolved_method,
     KlassHandle    resolved_klass,
     Handle         recv,
     KlassHandle    recv_klass,
     bool          check_null_and_abstract,
     TRAPS
    ) {
    
      int vtable_index = Method::invalid_vtable_index;
      methodHandle selected_method;
    
      // 当方法定义在接口中时,表示是miranda方法
      if (resolved_method->method_holder()->is_interface()) { 
        vtable_index = vtable_index_of_interface_method(resolved_klass,resolved_method);
    
        InstanceKlass* inst = InstanceKlass::cast(recv_klass());
        selected_method = methodHandle(THREAD, inst->method_at_vtable(vtable_index));
      } else {
        // 如果走如下的代码逻辑,则表示resolved_method不是miranda方法,需要动态分派且肯定有正确的vtable索引
        vtable_index = resolved_method->vtable_index();
    
        // 有些方法虽然看起来需要动态分派,但是如果这个方法有final关键字时,可进行静态绑定,所以直接调用即可
        // final方法其实不会放到vtable中,除非final方法覆写了父类中的方法
        if (vtable_index == Method::nonvirtual_vtable_index) {
          selected_method = resolved_method;
        } else {
          // 根据vtable和vtable_index以及inst进行方法的动态分派
          InstanceKlass* inst = (InstanceKlass*)recv_klass();
          selected_method = methodHandle(THREAD, inst->method_at_vtable(vtable_index));
        }
      }  
     
      // setup result  resolve的类型为CallInfo,为CallInfo设置了连接后的相关信息
      result.set_virtual(resolved_klass, recv_klass, resolved_method, selected_method, vtable_index, CHECK);
    }

    当为miranda方法时,调用 LinkResolver::vtable_index_of_interface_method()函数查找;当为final方法时,因为final方法不可能被子类覆写,所以resolved_method就是目标调用方法;除去前面的2种情况后,剩下的方法就需要结合vtable和vtable_index进行动态分派了。

    如上函数将查找到调用时需要的所有信息并存储到CallInfo类型的result变量中。 

    在获取到调用时的所有信息并存储到CallInfo中后,就可以根据info中相关信息填充ConstantPoolCacheEntry。我们回看InterpreterRuntime::resolve_invoke()函数的执行逻辑。

    InterpreterRuntime::resolve_invoke()函数第2部分:

    switch (info.call_kind()) {
      case CallInfo::direct_call: // 直接调用
        cache_entry(thread)->set_direct_call(
    		  bytecode,
    		  info.resolved_method());
        break;
      case CallInfo::vtable_call: // vtable分派
        cache_entry(thread)->set_vtable_call(
    		  bytecode,
    		  info.resolved_method(),
    		  info.vtable_index());
        break;
      case CallInfo::itable_call: // itable分派
        cache_entry(thread)->set_itable_call(
    		  bytecode,
    		  info.resolved_method(),
    		  info.itable_index());
        break;
      default:  ShouldNotReachHere();
    }
      

    无论直接调用,还是vtable和itable动态分派,都会在方法解析完成后将相关的信息存储到常量池缓存项中。调用cache_entry()函数获取对应的ConstantPoolCacheEntry项,然后调用set_vtable_call()函数,此函数会调用如下函数更新ConstantPoolCacheEntry项中的信息,如下:

    void ConstantPoolCacheEntry::set_direct_or_vtable_call(
     Bytecodes::Code  invoke_code,
     methodHandle     method,
     int              vtable_index
    ) {
      bool is_vtable_call = (vtable_index >= 0);  // FIXME: split this method on this boolean
     
      int byte_no = -1;
      bool change_to_virtual = false;
    
      switch (invoke_code) {
        case Bytecodes::_invokeinterface:
           change_to_virtual = true;
    
        // ...
        // 可以看到,通过_invokevirtual指令时,并不一定都是动态分发,也有可能是静态绑定
        case Bytecodes::_invokevirtual: // 当前已经在ConstantPoolCacheEntry类中了
          {
            if (!is_vtable_call) {
              assert(method->can_be_statically_bound(), "");
              // set_f2_as_vfinal_method checks if is_vfinal flag is true.
              set_method_flags(as_TosState(method->result_type()),
                               (                             1      << is_vfinal_shift) |
                               ((method->is_final_method() ? 1 : 0) << is_final_shift)  |
                               ((change_to_virtual         ? 1 : 0) << is_forced_virtual_shift), // 在接口中调用Object中定义的方法
                               method()->size_of_parameters());
              set_f2_as_vfinal_method(method());
            } else {
              // 执行这里的逻辑时,表示方法是非静态绑定的非final方法,需要动态分派,则vtable_index的值肯定大于等于0
              set_method_flags(as_TosState(method->result_type()),
                               ((change_to_virtual ? 1 : 0) << is_forced_virtual_shift),
                               method()->size_of_parameters());
              // 对于动态分发来说,ConstantPoolCacheEntry::_f2中保存的是vtable_index
              set_f2(vtable_index);
            }
            byte_no = 2;
            break;
          }
          // ...
      }
    
      if (byte_no == 1) {
        // invoke_code为非invokevirtual和非invokeinterface字节码指令
        set_bytecode_1(invoke_code);
      } else if (byte_no == 2)  {
        if (change_to_virtual) {
          if (method->is_public()) 
             set_bytecode_1(invoke_code);
        } else {
          assert(invoke_code == Bytecodes::_invokevirtual, "");
        }
        // set up for invokevirtual, even if linking for invokeinterface also:
        set_bytecode_2(Bytecodes::_invokevirtual);
      } 
    }
    

    连接完成后ConstantPoolCacheEntry中的各个项如下图所示。

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

    推荐阅读:

    第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

  • 相关阅读:
    Powered by .NET Core 进展0815:第5次发布尝试(Windows部署)团队
    峰回路转:去掉 DbContextPool 后 Windows 上的 .NET Core 版博客表现出色团队
    做梦也没有想到:Windows 上的 .NET Core 版博客系统表现更糟糕团队
    全网最详细的zkfc启动以后,几秒钟以后自动关闭问题的解决办法(图文详解)
    全网最详细的HBase启动以后,HMaster进程启动了,几秒钟以后自动关闭问题的解决办法(图文详解)
    全网最详细的启动或格式化zkfc时出现java.net.NoRouteToHostException: No route to host ... Will not attempt to authenticate using SASL (unknown error)错误的解决办法(图文详解)
    全网最详细的HA集群的主节点之间的双active,双standby,active和standby之间切换的解决办法(图文详解)
    全网最详细的启动zkfc进程时,出现INFO zookeeper.ClientCnxn: Opening socket connection to server***/192.168.80.151:2181. Will not attempt to authenticate using SASL (unknown error)解决办法(图文详解)
    全网最详细的再次或多次格式化导致namenode的ClusterID和datanode的ClusterID之间不一致的问题解决办法(图文详解)
    执行bin/hdfs haadmin -transitionToActive nn1时出现,Automatic failover is enabled for NameNode at bigdata-pro02.kfk.com/192.168.80.152:8020 Refusing to manually manage HA state的解决办法(图文详解)
  • 原文地址:https://www.cnblogs.com/mazhimazhi/p/15474323.html
Copyright © 2011-2022 走看看