假设在我的主方法中,我有一行代码LinkedList<E> myLinkedList = new LinkedList<>()
,所以现在我有一个名为myLinkedList1
的对象引用/指针变量(构造函数存储在另一个.java
文件中的LinkedList类中,而不是主方法所在的.java
文件)。
现在,我制作了另一个名为myLinkedList2
的引用/指针变量。我使用方法addLast(E newElement)
(这个方法当然存储在LinkedList
类中),但我只在myLinkedList1
上使用它(所以它是myLinkedList.addLast(E newElement)
),JVM怎么知道只在myLinkedList1
上使用这个方法而不在myLinkedList2
上使用,对象方法和它一起存储在堆中吗?我以为他们已经被放在堆栈上了。
内存中的一个对象包含以下信息:
- 一个指针,指向该对象所属的实际类
- 所有田地都有足够的空间。考虑到java是基于引用的,每个字段最多是64位——它们都是固定大小的,所以这并不复杂
- 其他与你的问题无关的东西
至关重要的是,它们根本不包含任何方法。
我以为它们已经放在堆栈上了。
方法?在堆栈上?这毫无意义。你一定是被误导了。方法不在堆栈中。它们也不是真的堆在一起。它们作为singleton存在于类定义中,该类定义对于任何类只加载一次。在现代JVM上,从技术上讲,这些JVM确实存在于堆中,但至关重要的是,它们离用于存储对象的堆空间很远。它们位于堆空间中,专门用于存储类的定义(字节码,或者更确切地说,转换的、热点的等字节码)。无论您创建多少个LinkedList实例,都只有一个LinkedListclass,因此100万个LinkedList实例仍然意味着您在内存中只存储了一次addLast
方法的实际主体内容。是的,addLast
是一个实例方法。内存中仍然只有它的一个副本(与实例字段不同;每个实例都有自己的非静态字段副本)。
任何给定的类在整个JVM中最多加载一次(为什么要多次加载?这些东西都是常量,这会浪费内存)。类包含所有方法(实例和静态)。
事实上,就方法而言,就JVM而言,静态方法和非静态方法之间没有任何区别。实例方法的第一个参数就是它的"receiver"——例如,String
的toLowerCase()
方法是一个采用1个参数的方法,类型为String
。之间的差异非常小
public String toLowerCase() {
return this.doTheThing();
}
和
public static String toLowerCase(String in) {
return in.doTheThing();
}
因此,当您在java中编写foo.bar();
时,您会得到两个不相关的步骤:首先,javac
将其转换为字节码,存储在类文件中。然后,5天后,在一台完全不同的机器上,有人运行你的类文件,然后JVM看到字节码并运行它
javac
首先通过检查foo
的类型来确定你在那里调用的是哪一个精确的bar()
。一旦javac
确定了这一点,你就会得到字节码:
INVOKEVIRTUAL com.pkg.FullTypeOfWhateverFooIsThere :: bar :: ()V
第三位是"签名"(参数类型和返回类型,在java中它们是方法标识的固有部分)。就是这样——所有事情的论据都在堆栈上。这个特殊的方法有一个参数(receiver——com.pkg.FullTypeOfWhateverFooIsThere
的一个实例),它必须在堆栈上。javac
确保它是真的。JVM检查字节码,如果不能确认它是真的,它将用VerifierError拒绝类文件(除非手动处理字节码或磁盘损坏,否则这种情况不会发生)。
然后,JVM"跟随指针",检查第一个参数的实际类型是什么,然后会找到代表该精确类型的加载类。然后,它检查该类(而不是com.pkg.FullTypeOfWhateverFooIsThere
-至少,如果实际的类是子类,则不是)是否有签名为()V
的名为foo
的方法。如果它找到了它,它就会运行它。如果它没有找到,它就会在层次结构中向上一个类,并一直寻找foo::()V
,直到找到为止(它会找到的,否则你的代码一开始就不会编译)。
当addLast
的代码运行时,当该方法开始执行时,堆栈上有两件事:
LinkedList
或其某些子类的实例- Object类型的新元素。(在JVM级别,泛型被删除)
该方法可以很好地完成它的工作;LinkedList有存储这些数据的字段,addLast的代码将与这些字段交互,以便执行其javadoc所说的应该执行的操作。特别是,LinkedList具有一个"head"字段,该字段指向一个节点,该节点包含对该对象(将是列表中的第一个对象)的引用和对另一个节点的指针。addLast代码保持循环,获取"下一个指针",直到下一个指示器为null。此时,它会创建一个新的Node对象,将其"值"设置为堆栈上的第二个对象,然后更新上次访问节点的"下一个"指针,使其指向新创建的节点,然后就完成了。
因此:
JVM只需要"找到"
addLast
代码,就可以知道要使用哪种方法(在字节码中),并在调用INVOKEVIRTUAL命令时,在内存中为实际类型的栈顶(好吧,在参数下面)找到一个指向单例"加载类"的指针,这很容易,因为所有对象都有一个指向它的指针,所以只需要查找它。因此,JVM可以执行addLast。addLast
代码只需要知道要对哪个列表进行操作,就是..列表。接收器作为第一个参数传递:foo.addLast(elem)
最终被调用,堆栈上有第一个foo
,然后是elem
。这与具有签名的静态方法(addLast(LinkedList<E> list, E elem)
)没有什么不同——任何对这种方法的调用在开始执行时都会在堆栈上有两个东西(静态方法没有接收器,它们只有的参数在堆栈上)。
您可以将在上调用方法的对象(即.
之前的对象)视为函数的额外参数。因此,从概念上讲,你可以认为myLinkedList1.addLast(elt)
有点像
LinkedList.addLast(myLinkedList1, elt)
因此;调用方";是传递给该方法的附加信息。有些语言会明确说明这一点。例如,在Lua中,foo:bar(1)
恰好等价于foo.bar(foo, 1)
,而在Python中,foo.bar(1)
大致等价于Foo.bar(foo, 1)
。但在Java中,这一切都发生在后台,而且有点复杂,但从概念上讲,这是相同的想法。