代码之家  ›  专栏  ›  技术社区  ›  Chris

为什么Java的虚拟现实需要解决调用方法的编译时间类?

  •  11
  • Chris  · 技术社区  · 16 年前

    考虑这个简单的Java类:

    class MyClass {
      public void bar(MyClass c) {
        c.foo();
      }
    }
    

    我想讨论C.foo()这行发生了什么。

    原始的,误导性的问题

    注意:并非所有这些都在每个单独的invokevirtual操作码中发生。提示:如果您想了解Java方法调用,不要只读取文档的调用!

    在字节码级别,c.foo()的主要部分将是invokeVirtual操作码,根据 the documentation for invokevirtual ,或多或少会发生以下情况:

    1. 查找中定义的foo方法 编译时间 我的班级。(这需要首先解析myClass。)
    2. 执行一些检查,包括:验证c不是初始化方法,并验证调用myclass.foo不会违反任何受保护的修饰符。
    3. 找出要实际调用的方法。特别是,查一下C's 运行时 类型。如果该类型具有foo(),则调用该方法并返回。如果没有,请查找c的运行时类型的超类;如果该类型有foo,请调用该方法并返回。如果没有,请查找c的运行时类型的超类;如果该类型有foo,请调用该方法并返回。等。。如果找不到合适的方法,则为错误。

    单是步骤3似乎就足以找出要调用的方法,并验证所述方法具有正确的参数/返回类型。所以我的问题是为什么第一步要执行。可能的答案似乎是:

    • 在步骤1完成之前,您没有足够的信息执行步骤3。(乍一看似乎难以置信,请解释。)
    • 在1和2中执行的链接或访问修饰符检查对于防止某些不好的事情发生至关重要, 这些检查必须基于编译时类型而不是运行时类型层次结构来执行。(请解释。)

    修正问题

    c.foo()行的javac编译器输出的核心是这样一条指令:

    invokevirtual i
    

    其中我是MyClass运行时常量池的索引。该常量池条目将是constant_methodref_info类型,并将指示(可能间接地)a)调用的方法的名称(即foo)、b)方法签名和c)调用该方法的编译时类的名称(即myClass)。

    问题是,为什么需要对编译时类型(myclass)的引用?由于invokevirtual将对c的运行时类型执行动态分派,所以存储对编译时类的引用不是多余的吗?

    5 回复  |  直到 16 年前
        1
  •  4
  •   Itay Maman    16 年前

    一切都是为了表现。当通过计算编译时类型(aka:static type)时,jvm可以计算运行时类型(aka:dynamic type)的虚拟函数表中被调用方法的索引。使用这个索引步骤3只需访问一个可以在恒定时间内完成的数组。不需要循环。

    例子:

    class A {
       void foo() { }
       void bar() { }
    }
    
    class B extends A {
      void foo() { } // Overrides A.foo()
    }
    

    默认情况下, A 延伸 Object 它定义了这些方法(当通过 invokespecial ):

    class Object {
      public int hashCode() { ... }
      public boolean equals(Object o) { ... }
      public String toString() { ... }
      protected void finalize() { ... }
      protected Object clone() { ... }
    }
    

    现在,考虑这个调用:

    A x = ...;
    x.foo();
    

    通过计算x的静态类型 jvm还可以找出此调用站点上可用的方法列表: hashCode , equals , toString , finalize , clone , foo , bar . 在这个名单上, 是第六个入口( 哈希码 是第一, 等于 是第二个等)。当jvm加载类文件时,索引的计算将执行一次。

    之后,每当jvm处理 x.foo() 只需要访问x提供的方法列表中的第6个条目,相当于 x.getClass().getMethods[5] ,(指 A.foo() 如果x的动态类型是 )并调用该方法。不需要穷尽地搜索这个方法数组。

    注意,不管x的动态类型是什么,方法的索引都保持不变,即:即使x指向b的实例,第6个方法仍然是 (尽管这次它将指向 B.foo() )

    更新

    [根据你的最新情况]:你是对的。为了执行虚拟方法分派,jvm需要的是方法的名称+签名(或vtable中的偏移量)。但是,jvm不会盲目地执行任务。它首先在一个名为 verification (也见) here )

    验证表达了jvm的设计原则之一: 它不依赖编译器生成正确的代码 . 它在允许执行代码之前检查代码本身。特别是,验证器检查每个调用的虚拟方法是否实际由接收方对象的静态类型定义。显然,执行这种检查需要接收器的静态类型。

        2
  •  1
  •   Rob Heiser    16 年前

    在阅读了文档之后,我并不是这样理解的。我认为您已经将步骤2和步骤3进行了转换,这将使整个系列事件更符合逻辑。

        3
  •  1
  •   Michael Ekstrand    16 年前

    据推测,编译器已经发生了1和2。我怀疑,至少部分目的是确保它们在运行时环境中仍然与类的版本保持一致,这可能与代码编译时所针对的版本不同。

    我还没消化 invokevirtual 不过,为了验证你的总结,罗布·海瑟可能是对的。

        4
  •  1
  •   Bert F    16 年前

    我猜答案是“B”。

    在1和2中执行的链接或访问修饰符检查对于防止某些不好的事情发生至关重要,这些检查必须基于编译时类型而不是运行时类型层次结构执行。(请解释。)

    #1由描述 5.4.3.3 Method Resolution ,这是一些重要的检查。例如,1检查编译时类型中方法的可访问性,如果不是,则可能返回illegalaccesserror:

    ……否则,如果引用的方法对d不可访问(5.4.4),方法解析将抛出一个illegalaccesserror。…

    如果只检查运行时类型(通过3),则运行时类型可能会非法扩展重写方法的可访问性(也称为“坏事”)。编译器确实应该防止这种情况,但jvm仍然在保护自己不受恶意代码(例如,手动构造的恶意代码)的攻击。

        5
  •  0
  •   Chris    16 年前

    要完全理解这些东西,您需要了解方法解析在Java中是如何工作的。如果你正在寻找一个深入的解释,我建议看这本书,“在Java虚拟机内部”。第8章“链接模型”的以下章节可在线查阅,似乎特别相关:

    (constant_methodref_info entries是类文件头中描述该类调用的方法的项。)

    感谢ITAY鼓励我用谷歌搜索找到这个。

    推荐文章