当调用一个对象的示例方法时,JVM中到底发生了什么?

hgqdbh6s  于 2022-11-07  发布在  其他
关注(0)|答案(1)|浏览(186)

我想我终于找到了如何词,是什么给我这么多的麻烦在理解:虚拟机如何访问类方法并仅在给定示例(对象)上使用它,同时捕获虚拟机仅被给予引用/指针变量。
与堆栈/堆交互的方法的大多数可视化(向大多数初学Java程序员显示)都没有达到我所寻求的深度,这一事实使这一点变得更加复杂。
我做了很多研究,我想对我学到的东西做一个很好的总结,我想问你是否可以纠正我的错误(或者如果您认为还有更多内容需要说明,请进一步详细说明)!请注意,我使用的是我找到的一篇文章的这一部分(我更多的是把它作为一个视觉参考,我理解文章中的一些文字与问题无关),所以请在阅读之前先看一下它:

假设我有一个引用/指针变量foo1,它的类型为Foo(使用一个名为Foo的构造函数创建)。foo1存储在堆栈中,但它指向的对象存储在堆中(Foo对象有一个示例变量int size;)。
因此,我理解了foo1.size如何给予size的整数值,因为foo1的值被解引用以获得size的值字段(reference/pointer变量有一个直接地址,其中size字段存储在对象的堆中)。
但是当foo1.bar()被运行时,它的字节码到底被翻译成什么呢?这个方法调用在运行时是如何执行的(如果说foo1的值被解引用以获得方法bar(),这是正确的吗?)
它是否与上图中的图表正确关联(全部在JVM中:它是否从栈上的引用/指针变量foo1到堆,堆实际上是指向方法区域中的另一个指针(指向所有类数据的字节码)full class data(在method table中,它只是指向每个示例方法的数据的指针数组,该示例方法可以在该类的对象上调用)的指针,然后堆本身具有“指针变量”到实际字节码method data)?
我为这篇文章的冗长而道歉,但是我想非常具体地说,因为我在过去的一周里遇到了很大的麻烦,试图正确地表达我的问题。我知道我听起来对我所引用的文章持怀疑态度,但是似乎有很多垃圾的可视化,我想确保我继续正确地进行Java编程,而不是基于错误的概念。

pcww981p

pcww981p1#

普通的示例方法调用被编译为invokevirtual指令。
这在JVMS §3.7.调用方法中有描述:
执行严修方法的一般方法引动过程会在对象的执行阶段型别上传送。(在C++中,它们是虚拟的。)这样的调用是使用 invokevirtual 指令实现的,该指令将运行时常量池条目的索引作为其参数,该索引给出对象的类类型的二进制名称的内部形式、要调用的方法的名称和该方法的描述符(参见4.3.3节)。要调用addTwo方法(前面定义为示例方法),我们可以写:

int add12and13() {
    return addTwo(12, 13);
}

这将编译为:

Method int add12and13()
0   aload_0             // Push local variable 0 (this)
1   bipush 12           // Push int constant 12
3   bipush 13           // Push int constant 13
5   invokevirtual #4    // Method Example.addtwo(II)I
8   ireturn             // Return int on top of operand stack;
                        // it is the int result of addTwo()

首先将reference推入到当前示例this的操作数堆栈中,然后推入方法调用的参数int1213。创建addTwo方法的框架后,传递给该方法的参数成为新帧的局部变量的初始值。也就是说,thisreference和两个参数,由调用程序压入操作数堆栈,将成为被调用方法的局部变量012的初始值。
如何在运行时执行调用取决于具体的JVM实现,但使用vtable是非常常见的。这基本上与您的问题中的图形相匹配。对接收器对象的引用,将成为被调用方法的this引用,用于检索方法表。
在HotSpot JVM中,元数据结构称为Klass(实际上是一个通用名称,即使在不同的实现中也是如此)。请参见OpenJDK Wiki上的“Object header layout”:
对象头由一个本机大小的标记字、一个klass字、一个32位长度的字(如果对象是数组)、一个32位间隔(如果对齐规则需要)以及零个或多个示例字段、数组元素或元数据字段组成。(有趣的小知识:Klass元对象包含一个紧跟在klass单词后面的C++ vtable。)
当解析一个对方法的符号引用时,它在表中的对应索引将被识别并记住,以供后续调用使用,因为它永远不会改变。然后,实际对象的类的条目可以用于调用。子类将具有超类的条目,新方法被追加到末尾,被覆盖方法的条目被替换。
这是一个简单的、未经优化的场景。大多数运行时优化在方法被内联的情况下工作得更好,在一段代码中有调用者和被调用者的上下文来转换。因此,HotSpot JVM将尝试内联甚至是 invokevirtual 指令到潜在的可重写方法。正如维基所说:

  • 如果类层次结构允许的话,虚(和接口)调用通常被降级为“特殊”调用。如果进一步的类加载破坏了事情,则注册依赖关系。
  • 具有不平衡类型配置文件的虚拟(和接口)调用使用乐观检查进行编译,以支持历史上常见的类型(或两种类型)。
  • 根据配置文件的不同,乐观检查失败将导致优化失败或通过(缓慢的)vtable/itable调用运行。
  • 在乐观类型调用的快速路径上,内联是常见的。最好的情况是内联的事实上的单态调用。这样的调用,如果是背对背的,将只执行一次接收者类型检查。

这种积极的或乐观的内联有时需要去优化,但通常会产生更高的总体性能。

相关问题