如何使用和存储对象的实例方法?

How are an object's instance methods used and stored?

假设在我的主要方法中我有代码行 LinkedList<E> myLinkedList = new LinkedList<>(),所以现在我有一个名为 myLinkedList1 的 reference/pointer 变量到一个对象(并且构造函数存储在它自己的 LinkedList class 在另一个 .java 文件中 与主要方法所在的文件不同 .java

现在我创建了另一个名为 myLinkedList2 的 reference/pointer 变量。我使用方法addLast(E newElement)(这个方法当然是存储在LinkedListclass),但是我只在myLinkedList1上使用(所以是myLinkedList.addLast(E newElement) ),JVM 怎么知道只在 myLinkedList1 而不是 myLinkedList2 上使用此方法,对象方法是否与它一起存储在堆中?我以为他们被放在了堆栈上。

您可以将调用方法 on 的对象(即 . 之前的对象)视为函数的额外参数。所以,从概念上讲,你可以认为 myLinkedList1.addLast(elt) 有点像

LinkedList.addLast(myLinkedList1, elt)

所以“invocant”是传递给方法的附加信息。有些语言使这一点变得明确。例如,在 Lua 中,foo:bar(1) 完全 等同于 foo.bar(foo, 1),而在 Python 中,foo.bar(1) 大致等价至 Foo.bar(foo, 1)。但是在Java中,这一切都发生在后台,有点复杂,但概念上是一样的。

内存中的对象包含以下信息:

  • 指向此对象实例的实际 class 的指针。
  • 所有字段都有足够的空间。鉴于 java 是基于引用的,每个字段最多为 64 位 - 它们都是固定大小,所以这并不复杂。
  • 其他与您的问题无关的内容。

重要的是它们根本不包含任何方法

I thought they were put on the stack.

方法?在堆栈上?这是没有意义的。你一定是被误导了。方法不在堆栈上。它们也不是真正的堆。它们在 class 定义中作为单例存在,对于任何 class 只加载一次。在现代 JVM 上,那些在技术上确实存在于堆中,但至关重要的是,离专用于存储对象的堆 space 不远。它们位于堆 space 中,专门用于存储 class 的定义(字节码,或者更确切地说,转换后的、热点化的字节码等字节码)。无论您创建多少个 LinkedList 实例,都只有一个 LinkedList class,因此 100 万个 LinkedList 实例仍然意味着您只将 addLast 方法的实际主体内容存储在内存中一次。是的,addLast是一个实例方法。内存中仍然只有它的一个副本(与实例字段不同;每个实例都有每个 non-static 字段的副本)。

对于整个 JVM,任何给定的 class 最多加载一次 (为什么要加载不止一次?这些东西是常量,这会是一种浪费内存)。 class 包含 所有方法 (实例和静态)。

事实上,就方法而言,就JVM而言,静态方法和non-static方法之间没有任何区别。一个实例方法只是将它的 'receiver' 作为第一个参数 - 例如,StringtoLowerCase() 方法是一个接受 1 个参数的方法,类型为 String非常小区别:

public String toLowerCase() {
  return this.doTheThing();
}

public static String toLowerCase(String in) {
  return in.doTheThing();
}

因此,当您在 java、foo.bar(); 中编写时,您会得到 2 个不相关的步骤:首先,javac 将其转换为字节码,存储在 class 文件。然后,5 天后,有人在一台完全不同的机器上运行您的 class 文件,然后 JVM 看到字节码并运行它。

javac 首先尝试通过检查 foo 的类型来确定您在那里调用的是哪个精确 bar() 。一旦 javac 弄明白了,你就会得到字节码:

INVOKEVIRTUAL com.pkg.FullTypeOfWhateverFooIsThere :: bar :: ()V

第三位是 'signature'(参数类型和 return 类型,在 java 中是方法标识的固有部分)。就是这样 - 所有事物的参数都在堆栈上。这个特定的方法有一个参数(接收者 - com.pkg.FullTypeOfWhateverFooIsThere 的一个实例),它必须在堆栈上。 javac 确保它是真实的。 JVM 检查字节码,如果它不能确认它是真的,它将拒绝带有 VerifierError 的 class 文件(除非您手动弄乱字节码,或者磁盘损坏,否则不会发生这种情况)。

然后,JVM 'follows the pointer' 并检查第一个参数的 actual 类型是什么,然后将找到表示该精确类型的已加载 class .然后它检查 class(而不是 com.pkg.FullTypeOfWhateverFooIsThere - 至少,如果实际的 class 是子 class)一个名为 foo 的方法,签名为 ()V。如果找到它,它就会运行它。如果没有,它会在层次结构中上升一个 class 并继续寻找 foo::()V 直到它找到它(它会找到它,否则你的代码不会首先编译)。

addLast 的代码运行时,当该方法开始执行时,堆栈中有 2 个东西:

  1. LinkedList 或其子class 的实例。
  2. 一个对象类型的新元素。 (在 JVM 级别,泛型被删除)。

该方法可以很好地完成它的工作; LinkedLists 有存储这些数据的字段,addLast 的代码将与这些字段交互,以执行它的 javadoc 所说的它应该做的事情。具体来说,LinkedList 有一个 'head' 字段指向一个节点,该节点包含对该对象的引用(这将是列表中的第一个对象)和指向另一个节点的指针。 addLast 代码不断循环,获取 'next pointer',直到下一个指针为空。那时它创建了一个新的 Node 对象,将其 'value' 设置为堆栈中的第二个对象,然后更新最后访问的节点的 'next' 指针指向这个新创建的节点,然后完成了。

因此:

  • JVM 需要 'find' addLast 代码的所有内容,就是知道要使用哪个方法(在字节码中),以及指向单例的指针'loaded class' 在调用 INVOKEVIRTUAL 命令时实际类型为 top-of-stack 的内存中(好吧,在参数下面),这很容易,因为所有对象都有一个指向它的指针,所以这只是一个问题查找它。因此,JVM 可以执行 addLast

  • addLast 代码需要知道要对哪个列表进行操作的全部内容是……列表。接收器作为第一个参数传递:foo.addLast(elem) 最终被调用,堆栈上有第一个 foo,然后是 elem。这与具有签名 (addLast(LinkedList<E> list, E elem)) 的静态方法没有什么不同 - 对此类方法的任何调用在开始执行时也会在堆栈上有 2 个东西(静态方法没有接收器,它们 只是他们的参数在堆栈上)。