如何使用和存储对象的实例方法?
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)
(这个方法当然是存储在LinkedList
class),但是我只在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' 作为第一个参数 - 例如,String
的 toLowerCase()
方法是一个接受 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 个东西:
LinkedList
或其子class 的实例。
- 一个对象类型的新元素。 (在 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 个东西(静态方法没有接收器,它们 只是他们的参数在堆栈上)。
假设在我的主要方法中我有代码行 LinkedList<E> myLinkedList = new LinkedList<>()
,所以现在我有一个名为 myLinkedList1
的 reference/pointer 变量到一个对象(并且构造函数存储在它自己的 LinkedList class 在另一个 .java
文件中 与主要方法所在的文件不同 .java
。
现在我创建了另一个名为 myLinkedList2
的 reference/pointer 变量。我使用方法addLast(E newElement)
(这个方法当然是存储在LinkedList
class),但是我只在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' 作为第一个参数 - 例如,String
的 toLowerCase()
方法是一个接受 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 个东西:
LinkedList
或其子class 的实例。- 一个对象类型的新元素。 (在 JVM 级别,泛型被删除)。
该方法可以很好地完成它的工作; LinkedLists 有存储这些数据的字段,addLast 的代码将与这些字段交互,以执行它的 javadoc 所说的它应该做的事情。具体来说,LinkedList 有一个 'head' 字段指向一个节点,该节点包含对该对象的引用(这将是列表中的第一个对象)和指向另一个节点的指针。 addLast 代码不断循环,获取 'next pointer',直到下一个指针为空。那时它创建了一个新的 Node 对象,将其 'value' 设置为堆栈中的第二个对象,然后更新最后访问的节点的 'next' 指针指向这个新创建的节点,然后完成了。
因此:
JVM 需要 'find'
addLast
代码的所有内容,就是知道要使用哪个方法(在字节码中),以及指向单例的指针'loaded class' 在调用 INVOKEVIRTUAL 命令时实际类型为 top-of-stack 的内存中(好吧,在参数下面),这很容易,因为所有对象都有一个指向它的指针,所以这只是一个问题查找它。因此,JVM 可以执行 addLastaddLast
代码需要知道要对哪个列表进行操作的全部内容是……列表。接收器作为第一个参数传递:foo.addLast(elem)
最终被调用,堆栈上有第一个foo
,然后是elem
。这与具有签名 (addLast(LinkedList<E> list, E elem)
) 的静态方法没有什么不同 - 对此类方法的任何调用在开始执行时也会在堆栈上有 2 个东西(静态方法没有接收器,它们 只是他们的参数在堆栈上)。