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

Zen*_*yro 0 java methods jvm

我想我终于找到了如何表达,是什么让我在理解上遇到这么多麻烦:虚拟机如何访问类方法并仅在给定实例(对象)上使用它,但虚拟机仅是被赋予引用/指针变量。

\n

更糟糕的是,大多数与堆栈/堆交互的方法的可视化(向大多数 Java 初学者展示的)都没有深入到我正在寻找的深度。

\n

我做了很多研究,我想对我所学到的内容做一个很好的总结,我想问您是否可以纠正我的错误之处(或者如果您认为还有更多可以说的,请进一步详细说明) !请注意,我正在使用我找到的一篇文章的这一部分(我更多地使用它作为视觉参考,我理解文章中的一些文字与问题无关),所以请在继续阅读之前先看一下它:

\n

在此输入图像描述

\n

foo1所以让\xe2\x80\x99s 说我有一个类型的引用/指针变量Foo(是使用名为 的构造函数创建的Foo)。foo1存储在栈上,但它指向的对象存储在堆上(Foo具有实例变量的对象int size;)。

\n

所以我明白如何foo1.size给出 的整数值,size因为 的值foo1被取消引用以获取 的值字段size(引用/指针变量有一个直接地址,该size字段存储在对象的堆上)。

\n

但是当foo1.bar()运行时,它的字节码到底翻译成什么呢?foo1这个方法调用在运行时是如何执行的(说 的值被取消引用为 get 方法是否正确bar())?

\n

它是否与上图中的图表正确相关(全部在 JVM 中:它是否从foo1堆栈上的引用/指针变量到堆,该变量实际上是指向另一个指针的指针(该指针指向所有类的字节码) data)full class data(在method table方法区域中,它只是指向可以在该类的对象上调用的每个实例方法的数据的指针数组,然后方法区域本身具有指向实际字节码的“指针变量”)method data

\n

我对这篇文章的冗长表示歉意,但我想非常具体,因为过去一周我在试图正确表达我的问题时遇到了很大的麻烦。我知道我对我引用的文章持怀疑态度,但似乎有很多垃圾可视化,我想确保我\xe2\x80\x99m 正确地继续我的 Java 编程,而不是基于错误的概念。

\n

Hol*_*ger 5

普通实例方法调用被编译为invokevirtual指令。

\n

这已在JVMS\xc2\xa73.7 中进行了描述。调用方法

\n
\n

实例方法的正常方法调用在对象的运行时类型上分派。(在 C++ 术语中,它们是虚拟的。)这样的调用是使用invokevirtual指令实现的,该指令将运行时常量池条目的索引作为其参数,给出对象的类类型的二进制名称的内部形式、要调用的方法的名称以及该方法的描述符 ( \xc2\xa74.3.3 )。要调用addTwo之前定义为实例方法的方法,我们可以编写:

\n
int add12and13() {\n    return addTwo(12, 13);\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这编译为:

\n
Method int add12and13()\n0   aload_0             // Push local variable 0 (this)\n1   bipush 12           // Push int constant 12\n3   bipush 13           // Push int constant 13\n5   invokevirtual #4    // Method Example.addtwo(II)I\n8   ireturn             // Return int on top of operand stack;\n                        // it is the int result of addTwo()\n
Run Code Online (Sandbox Code Playgroud)\n

reference通过首先将 a 推入当前实例 到this操作数堆栈来设置调用。然后推送方法调用的参数、int12和,。13创建方法的框架时addTwo,传递给该方法的参数将成为新框架的局部变量的初始值。也就是说,调用者将referencefor和两个参数压入操作数堆栈,将成为被调用方法的局部变量、、 和的初始值。this012

\n
\n
\n

xe2x80x99取决于特定的JVM实现,如何在运行时执行调用,但使用vtable很常见的。这基本上符合你问题中的图形。对接收者对象的引用将成为this所调用方法的引用,用于检索方法表。

\n

在HotSpot JVM中,元数据结构被称为Klass(实际上是一个通用名称,即使跨不同的实现)。请参阅OpenJDK Wiki 上的 \xe2\x80\x9cObject 标头布局\xe2\x80\x9d

\n
\n

对象头由一个本机大小的标记字、一个 klass 字、一个 32 位长度字(如果对象是数组)、一个 32 位间隙(如果对齐规则需要)以及零个或多个实例组成字段、数组元素或元数据字段。(有趣的琐事:Klass 元对象在 klass 单词之后立即包含一个 C++ vtable。)

\n
\n

当解析对方法的符号引用时,表中相应的索引将被识别并记住以供后续调用,因为它永远不会改变。然后,可以使用实际对象\xe2\x80\x99s类的入口来进行调用。子类将具有超类的条目,新方法附加到末尾,并替换重写方法的条目。

\n
\n

这是简单的、未经优化的场景。大多数运行时优化在方法内联时效果更好,以便将调用者和被调用者的上下文放在一段代码中进行转换。因此,HotSpot JVM 甚至会尝试将invokevirtual指令内联到可能可重写的方法。正如维基百科所说

\n
\n
    \n
  • 如果类层次结构允许,虚拟(和接口)调用通常会降级为“特殊”调用。注册一个依赖项,以防进一步的类加载破坏事情。
  • \n
  • 具有不平衡类型配置文件的虚拟(和接口)调用通过乐观检查进行编译,以支持历史上常见的类型(或两种类型)。
  • \n
  • 根据配置文件,乐观检查失败将导致优化或通过(缓慢的)vtable/itable 调用运行。
  • \n
  • 在乐观类型调用的快速路径上,内联很常见。最好的情况是事实上的单态调用,它是内联的。如果连续调用,此类调用将仅执行一次接收器类型检查。
  • \n
\n
\n

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

\n

  • 我不知道你对这张图有什么具体的问题。据我所知,它看起来是正确的(这不是随着时间的推移进行操作的最佳图表类型)。 (2认同)
  • `getfield` 从操作数堆栈中弹出要使用的对象引用,这是前面的 `aload_0` 指令的结果,即 `this` 实例。因为对于非“静态”方法,变量“0”在方法入口处使用“this”进行预初始化。这是调用者向“invokevirtual”指令提供的引用(对于“invokeinterface”和“invokespecial”的作用相同)。 (2认同)
  • 在源代码中,该标识符始终是“`this`”... (2认同)