Zen*_*yro 0 java class linked-list heap-memory stack-memory
让\xe2\x80\x99s 在我的 main 方法中说我有代码行LinkedList<E> myLinkedList = new LinkedList<>()
,所以现在我有一个命名myLinkedList1
为对象的引用/指针变量(并且构造函数存储在另一个.java
文件中自己的 LinkedList 类中,而不是同一个.java
文件主要方法在)。
现在我创建另一个名为 的引用/指针变量myLinkedList2
。我使用了方法addLast(E newElement)
(这个方法当然是存储在LinkedList
类中的),但是我只在on上使用myLinkedList1
(所以是这样myLinkedList.addLast(E newElement)
),JVM怎么知道只在on上使用这个方法myLinkedList1
而不是myLinkedList2
在堆上存储的对象方法用它?我以为它们被放在堆栈上。
内存中的对象包含以下信息:
至关重要的是,它们根本不包含任何方法。
我以为它们被放在堆栈上。
方法?在堆栈上?这是没有意义的。你一定是被误导了。方法不在堆栈上。它们也不真正在堆中。它们在类定义中作为单例存在,对于任何类仅加载一次。在现代 JVM 上,从技术上讲,它们确实存在于堆中,但最重要的是,它们远离专用于存储对象的堆空间。它们位于专门用于存储类的定义(字节码,或者更确切地说,转换后的字节码、热点字节码等)的堆空间中。无论您创建多少个 LinkedList 实例,都只有一个 LinkedList 类,因此 100 万个 LinkedList 实例仍然意味着您addLast
在内存中只存储了一次方法的实际主体内容。是的,addLast
是一个实例方法。内存中仍然只有一份副本(与实例字段不同;每个实例都有自己的每个非静态字段的副本)。
任何给定的类对于整个 JVM最多加载一次(为什么要加载多次?这些东西是不变的,这会浪费内存)。类包含所有方法(实例和静态)。
事实上,就方法而言,就 JVM 而言,静态方法和非静态方法没有任何区别。实例方法仅将其“接收者”作为第一个参数 - 例如,String
的toLowerCase()
方法是一个采用 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
()V
foo::()V
当代码addLast
运行时,当该方法开始执行时,堆栈上有两件事:
LinkedList
或其某个子类。该方法可以很好地完成它的工作;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 个东西(静态方法没有接收器,它们只是在堆栈上有它们的参数)。
归档时间: |
|
查看次数: |
653 次 |
最近记录: |