我想我终于找到了如何词,是什么给我这么多的麻烦在理解:虚拟机如何访问类方法并仅在给定示例(对象)上使用它,同时捕获虚拟机仅被给予引用/指针变量。
与堆栈/堆交互的方法的大多数可视化(向大多数初学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编程,而不是基于错误的概念。
1条答案
按热度按时间pcww981p1#
普通的示例方法调用被编译为
invokevirtual
指令。这在JVMS §3.7.调用方法中有描述:
执行严修方法的一般方法引动过程会在对象的执行阶段型别上传送。(在C++中,它们是虚拟的。)这样的调用是使用 invokevirtual 指令实现的,该指令将运行时常量池条目的索引作为其参数,该索引给出对象的类类型的二进制名称的内部形式、要调用的方法的名称和该方法的描述符(参见4.3.3节)。要调用
addTwo
方法(前面定义为示例方法),我们可以写:这将编译为:
首先将
reference
推入到当前示例this
的操作数堆栈中,然后推入方法调用的参数int
值12
和13
。创建addTwo
方法的框架后,传递给该方法的参数成为新帧的局部变量的初始值。也就是说,this
的reference
和两个参数,由调用程序压入操作数堆栈,将成为被调用方法的局部变量0
、1
和2
的初始值。如何在运行时执行调用取决于具体的JVM实现,但使用vtable是非常常见的。这基本上与您的问题中的图形相匹配。对接收器对象的引用,将成为被调用方法的
this
引用,用于检索方法表。在HotSpot JVM中,元数据结构称为
Klass
(实际上是一个通用名称,即使在不同的实现中也是如此)。请参见OpenJDK Wiki上的“Object header layout”:对象头由一个本机大小的标记字、一个klass字、一个32位长度的字(如果对象是数组)、一个32位间隔(如果对齐规则需要)以及零个或多个示例字段、数组元素或元数据字段组成。(有趣的小知识:Klass元对象包含一个紧跟在klass单词后面的C++ vtable。)
当解析一个对方法的符号引用时,它在表中的对应索引将被识别并记住,以供后续调用使用,因为它永远不会改变。然后,实际对象的类的条目可以用于调用。子类将具有超类的条目,新方法被追加到末尾,被覆盖方法的条目被替换。
这是一个简单的、未经优化的场景。大多数运行时优化在方法被内联的情况下工作得更好,在一段代码中有调用者和被调用者的上下文来转换。因此,HotSpot JVM将尝试内联甚至是 invokevirtual 指令到潜在的可重写方法。正如维基所说:
这种积极的或乐观的内联有时需要去优化,但通常会产生更高的总体性能。