繁体   English   中英

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

[英]What exactly happens in the JVM when invoking an object's instance method?

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

与堆栈/堆交互的方法的大多数可视化(向大多数初学者 Java 程序员展示)的事实并没有达到我正在寻找的深度 go ,这使情况更加复杂。

我做了很多研究,我想对我学到的东西做一个很好的总结,我想问你是否可以纠正我的错误(如果你认为还有更多可以说的话,可以进一步阐述) ,请注意,我正在使用我找到的一篇文章的这一部分(我更多地将其用作视觉参考,我理解文章中的某些文本与问题无关):所以请在阅读前先看一下向前:

在此处输入图像描述

因此,假设我有一个Foo类型的引用/指针变量foo1 (使用名为Foo的构造函数创建)。 foo1存储在堆栈上,但它指向的 object 存储在堆上( Foo object 具有实例变量int size; )。

所以我理解foo1.size如何给出 integer 的size值,因为foo1的值被取消引用以获得size的值字段(引用/指针变量有一个直接地址,其中size字段存储在对象的堆上).

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

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

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

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

这已在JVMS §3.7 中进行了描述。 调用方法

实例方法的正常方法调用在 object 的运行时类型上分派。(它们是虚拟的,在 C++ 术语中。)这样的调用是使用invokevirtual指令实现的,它的参数是运行时的索引 -时间常量池条目给出了 class 类型的 object 的二进制名称的内部形式、要调用的方法的名称以及该方法的描述符( §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()

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


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

在 HotSpot JVM 中,元数据结构称为Klass (实际上是一个通用名称,即使在不同的实现中也是如此)。 请参阅OpenJDK Wiki 上的“对象 header 布局”

一个 object header 由一个 native-sized mark word,一个 klass word,一个 32-bit length word(如果 object 是一个数组),一个 32-bit gap(如果 alignment 规则需要),然后是零个或多个实例字段、数组元素或元数据字段。 (有趣的琐事:Klass 元对象在 klass 词后立即包含一个 C++ vtable。)

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


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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM