调用对象的实例方法时,JVM 中到底发生了什么?
What exactly happens in the JVM when invoking an object's instance method?
我想我终于找到了措辞,是什么让我难以理解:虚拟机如何访问 classes 方法并仅在给定实例(对象)上使用它) 但要注意的是虚拟机只被赋予了 reference/pointer 变量。
由于大多数与 stack/heap 交互的方法的可视化(向大多数初学者 Java 程序员展示)并没有深入到我的深度,这一事实使情况更加复杂。我在找
我做了很多研究,我想把我学到的东西做一个很好的总结,我想请问您是否可以纠正我的错误(如果您认为还有更多,请进一步说明可以这么说)!请注意,我正在使用我找到的一篇文章的这一部分(我更多地将其用作视觉参考,我理解文章中的某些文本与问题无关),因此请在继续阅读之前先看一下:
所以假设我有一个 reference/pointer 类型为 Foo
的变量 foo1
(使用名为 Foo
的构造函数创建)。 foo1
存储在栈上,但它指向的对象存储在堆上(Foo
对象有一个实例变量int size;
)。
所以我理解 foo1.size
将如何给出 size
的整数值,因为 foo1
的值被取消引用以获得 size
的值字段(reference/pointer变量有一个直接地址,其中size
字段存储在对象的堆上。
但是当 foo1.bar()
是 运行 时,它的字节码 t运行 到底是什么?以及这个方法调用是如何在运行时执行的(说 foo1
的值被取消引用以获取方法 bar()
是否正确)?
它是否与上图中的图表正确相关(全部在 JVM 中:它是否从堆栈上的 reference/pointer 变量 foo1
到实际上是指向的指针的堆另一个指针(指向所有 class 数据的字节码) full class data
(在 method table
中,它只是指向每个实例方法的数据的指针数组,可以在class) 在方法区中的对象,然后它本身具有指向实际字节码的“指针变量” method data
)?
对于这个 post 的冗长,我深表歉意,但我想非常具体,因为过去一周我在试图正确表达我的问题时遇到了很大的麻烦。我知道我听起来对我引用的文章持怀疑态度,但似乎那里有很多垃圾可视化,我想确保我继续我的 Java 编程是正确的,而不是基于错误的概念。
普通实例方法调用被编译为 invokevirtual
指令。
这已在 JVMS, §3.7. Invoking Methods 中描述:
The normal method invocation for a instance method dispatches on the run-time type of the object. (They are virtual, in C++ terms.) Such an invocation is implemented using the invokevirtual instruction, which takes as its argument an index to a run-time constant pool entry giving the internal form of the binary name of the class type of the object, the name of the method to invoke, and that method's descriptor (§4.3.3). To invoke the addTwo
method, defined earlier as an instance method, we might write:
int add12and13() {
return addTwo(12, 13);
}
This compiles to:
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()
The invocation is set up by first pushing a reference
to the current instance, this
, on to the operand stack. The method invocation's arguments, int
values 12
and 13
, are then pushed. When the frame for the addTwo
method is created, the arguments passed to the method become the initial values of the new frame's local variables. That is, the reference
for this
and the two arguments, pushed onto the operand stack by the invoker, will become the initial values of local variables 0
, 1
, and 2
of the invoked method.
这取决于特定的 JVM 实现,如何在运行时执行调用,但是使用 vtable 是很常见的。这基本上与您问题中的图形相符。对接收者对象的引用将成为调用方法的 this
引用,用于检索方法 table.
在 HotSpot JVM 中,元数据结构称为 Klass
(实际上是一个通用名称,即使在不同的实现中也是如此)。见 “Object header layout” on the OpenJDK Wiki:
An object header consists of a native-sized mark word, a klass word, a 32-bit length word (if the object is an array), a 32-bit gap (if required by alignment rules), and then zero or more instance fields, array elements, or metadata fields. (Interesting Trivia: Klass metaobjects contain a C++ vtable immediately after the klass word.)
当解析一个方法的符号引用时,它在 table 中的相应索引将被识别并记住以供后续调用使用,因为它永远不会改变。然后,可以使用实际对象的 class 的条目进行调用。 Subclasses 将有 superclass 的条目,新方法附加到末尾,替换覆盖方法的条目。
这是简单的、未优化的场景。大多数运行时优化在方法被内联时效果更好,以便在一段代码中转换调用者和被调用者的上下文。因此,HotSpot JVM 会尝试将 invokevirtual 指令内联到潜在的可覆盖方法。作为 the wiki says:
- Virtual (and interface) invocations are often demoted to "special" invocations, if the class hierarchy permits it. A dependency is registered in case further class loading spoils things.
- Virtual (and interface) invocations with a lopsided type profile are compiled with an optimistic check in favor of the historically common type (or two types).
- Depending on the profile, a failure of the optimistic check will either deoptimize or run through a (slow) vtable/itable call.
- On the fast path of an optimistically typed call, inlining is common. The best case is a de facto monomorphic call which is inlined. Such calls, if back-to-back, will perform the receiver type check only once.
这种积极或乐观的内联有时需要 Deoptimization 但通常会产生更高的整体性能。
我想我终于找到了措辞,是什么让我难以理解:虚拟机如何访问 classes 方法并仅在给定实例(对象)上使用它) 但要注意的是虚拟机只被赋予了 reference/pointer 变量。
由于大多数与 stack/heap 交互的方法的可视化(向大多数初学者 Java 程序员展示)并没有深入到我的深度,这一事实使情况更加复杂。我在找
我做了很多研究,我想把我学到的东西做一个很好的总结,我想请问您是否可以纠正我的错误(如果您认为还有更多,请进一步说明可以这么说)!请注意,我正在使用我找到的一篇文章的这一部分(我更多地将其用作视觉参考,我理解文章中的某些文本与问题无关),因此请在继续阅读之前先看一下:
所以假设我有一个 reference/pointer 类型为 Foo
的变量 foo1
(使用名为 Foo
的构造函数创建)。 foo1
存储在栈上,但它指向的对象存储在堆上(Foo
对象有一个实例变量int size;
)。
所以我理解 foo1.size
将如何给出 size
的整数值,因为 foo1
的值被取消引用以获得 size
的值字段(reference/pointer变量有一个直接地址,其中size
字段存储在对象的堆上。
但是当 foo1.bar()
是 运行 时,它的字节码 t运行 到底是什么?以及这个方法调用是如何在运行时执行的(说 foo1
的值被取消引用以获取方法 bar()
是否正确)?
它是否与上图中的图表正确相关(全部在 JVM 中:它是否从堆栈上的 reference/pointer 变量 foo1
到实际上是指向的指针的堆另一个指针(指向所有 class 数据的字节码) full class data
(在 method table
中,它只是指向每个实例方法的数据的指针数组,可以在class) 在方法区中的对象,然后它本身具有指向实际字节码的“指针变量” method data
)?
对于这个 post 的冗长,我深表歉意,但我想非常具体,因为过去一周我在试图正确表达我的问题时遇到了很大的麻烦。我知道我听起来对我引用的文章持怀疑态度,但似乎那里有很多垃圾可视化,我想确保我继续我的 Java 编程是正确的,而不是基于错误的概念。
普通实例方法调用被编译为 invokevirtual
指令。
这已在 JVMS, §3.7. Invoking Methods 中描述:
The normal method invocation for a instance method dispatches on the run-time type of the object. (They are virtual, in C++ terms.) Such an invocation is implemented using the invokevirtual instruction, which takes as its argument an index to a run-time constant pool entry giving the internal form of the binary name of the class type of the object, the name of the method to invoke, and that method's descriptor (§4.3.3). To invoke the
addTwo
method, defined earlier as an instance method, we might write:int add12and13() { return addTwo(12, 13); }
This compiles to:
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()
The invocation is set up by first pushing a
reference
to the current instance,this
, on to the operand stack. The method invocation's arguments,int
values12
and13
, are then pushed. When the frame for theaddTwo
method is created, the arguments passed to the method become the initial values of the new frame's local variables. That is, thereference
forthis
and the two arguments, pushed onto the operand stack by the invoker, will become the initial values of local variables0
,1
, and2
of the invoked method.
这取决于特定的 JVM 实现,如何在运行时执行调用,但是使用 vtable 是很常见的。这基本上与您问题中的图形相符。对接收者对象的引用将成为调用方法的 this
引用,用于检索方法 table.
在 HotSpot JVM 中,元数据结构称为 Klass
(实际上是一个通用名称,即使在不同的实现中也是如此)。见 “Object header layout” on the OpenJDK Wiki:
An object header consists of a native-sized mark word, a klass word, a 32-bit length word (if the object is an array), a 32-bit gap (if required by alignment rules), and then zero or more instance fields, array elements, or metadata fields. (Interesting Trivia: Klass metaobjects contain a C++ vtable immediately after the klass word.)
当解析一个方法的符号引用时,它在 table 中的相应索引将被识别并记住以供后续调用使用,因为它永远不会改变。然后,可以使用实际对象的 class 的条目进行调用。 Subclasses 将有 superclass 的条目,新方法附加到末尾,替换覆盖方法的条目。
这是简单的、未优化的场景。大多数运行时优化在方法被内联时效果更好,以便在一段代码中转换调用者和被调用者的上下文。因此,HotSpot JVM 会尝试将 invokevirtual 指令内联到潜在的可覆盖方法。作为 the wiki says:
- Virtual (and interface) invocations are often demoted to "special" invocations, if the class hierarchy permits it. A dependency is registered in case further class loading spoils things.
- Virtual (and interface) invocations with a lopsided type profile are compiled with an optimistic check in favor of the historically common type (or two types).
- Depending on the profile, a failure of the optimistic check will either deoptimize or run through a (slow) vtable/itable call.
- On the fast path of an optimistically typed call, inlining is common. The best case is a de facto monomorphic call which is inlined. Such calls, if back-to-back, will perform the receiver type check only once.
这种积极或乐观的内联有时需要 Deoptimization 但通常会产生更高的整体性能。