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

Zen*_*yro 0 java class linked-list heap-memory stack-memory

让\xe2\x80\x99s 在我的 main 方法中说我有代码行LinkedList<E> myLinkedList = new LinkedList<>(),所以现在我有一个命名myLinkedList1为对象的引用/指针变量(并且构造函数存储在另一个.java文件中自己的 LinkedList 类中,而不是同一个.java文件主要方法在)。

\n

现在我创建另一个名为 的引用/指针变量myLinkedList2。我使用了方法addLast(E newElement)(这个方法当然是存储在LinkedList类中的),但是我只在on上使用myLinkedList1(所以是这样myLinkedList.addLast(E newElement)),JVM怎么知道只在on上使用这个方法myLinkedList1而不是myLinkedList2在堆上存储的对象方法用它?我以为它们被放在堆栈上。

\n

rzw*_*oot 5

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

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

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

我以为它们被放在堆栈上。

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

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

事实上,就方法而言,就 JVM 而言,静态方法和非静态方法没有任何区别。实例方法仅将其“接收者”作为第一个参数 - 例如,StringtoLowerCase()方法是一个采用 1 个参数、类型为 的方法String。之间的差异很小

public String toLowerCase() {
  return this.doTheThing();
}
Run Code Online (Sandbox Code Playgroud)

public static String toLowerCase(String in) {
  return in.doTheThing();
}
Run Code Online (Sandbox Code Playgroud)

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

javac首先尝试bar()通过检查类型来确定您正在调用哪个精确值foo。一旦javac弄清楚了,你最终会得到字节码:

INVOKEVIRTUAL com.pkg.FullTypeOfWhateverFooIsThere :: bar :: ()V
Run Code Online (Sandbox Code Playgroud)

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

然后,JVM“跟随指针”并检查第一个参数的实际类型是什么,然后找到表示该精确类型的加载类。然后,它检查该类(至少,如果实际类是子类则不会)是否有以签名命名com.pkg.FullTypeOfWhateverFooIsThere的方法。如果找到它,它就会运行它。如果没有,它会在层次结构中向上移动一个类,并继续寻找,直到找到它(它会找到它,否则你的代码一开始就不会编译)。foo()Vfoo::()V

当代码addLast运行时,当该方法开始执行时,堆栈上有两件事:

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

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

因此:

  • JVM“查找”代码所需要做的addLast就是知道要使用哪个方法(在字节码中),以及指向内存中堆栈顶部实际类型的单例“加载类”的指针(嗯,在参数下面)当调用 INVOKEVIRTUAL 命令时,这很容易,因为所有对象都有一个指向它的指针,所以只需查找它即可。这样,JVM就可以执行addLast了。

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