[英]Java runtime memory model --
我正在寻找以下方面的验证/更正。
假设以下继承层次结构 -
class A {
void m1() { System.out.print(System.currentTimeMillis()); }
void m2() { System.out.print(new Date()); } // current date
}
class B extends A {
int x;
void m1() { System.out.print(x); } // overriding
void m99() { System.out.print(++x); }
}
还假设类B
在应用程序中的某个点被实例化,即执行以下语句 -
B b = new B();
(A 段)在构建应用程序时,类A
和B
都由静态加载器加载到内存中。 这两个类以及它们所有成员方法的定义都在内存中。
当B
用上面的语句实例化时,堆中的内存空间被分配给那个对象b
。 方法m1()
、 m2()
和m99()
在b
这个空间中都有它们的定义。
这些方法定义只是对存在于类定义中的方法“模板”的引用。 在类中,这些方法是操作序列——如果方法在任何参数和/或全局变量上执行,则参数化操作。
当其中一个方法,比如说b.m99()
在运行时被调用时,JRE 转到B
类定义以获取该“模板”(操作序列),查找b
字段的当前值,并填写具有这些字段的当前值的“模板”还将这些当前值推送到堆栈空间,并通过执行在类定义中找到的这些操作来运行方法。
如果该方法是从超类继承的,例如。 上面的m2()
,类中该方法的定义(上面 A 段中提到的定义)本身就是对类 A 中m2()
定义的引用。
在运行时,当b.m2()
被执行时,JRE 会直接进入A
类以找到要执行的低级操作的“模板”。
这些对方法定义的引用在编译时被检查并放入字节码中。 例如。 在字节码为上述的情况下,类B
有,直接引用方法m2()
类的A
用于方法m2()
它继承A
。
这一切都准确吗? 如果没有,在哪里/为什么不呢?
一般来说,Java 的执行环境可以通过多种方式实现,不可能说“Java”一般是做什么的。
构建应用程序时,类
A
和B
都由静态加载器加载到内存中。 这两个类以及它们所有成员方法的定义都在内存中。
部署的标准方式是将 Java 源代码编译为字节码。 当应用程序被执行时,类将被加载。 没有“静态加载器”这样的东西。 有不同的类加载器。 当类文件在类路径上传递时,它们将由应用程序类加载器加载。
当
B
用上面的语句实例化时,堆中的内存空间被分配给那个对象b
。 方法m1()
、m2()
和m99()
在b
这个空间中都有它们的定义。
正如Andreas 所说,方法定义是 JVM 类表示的一部分。 该对象仅包含对类的引用(指针)。
这些方法定义只是对存在于类定义中的方法“模板”的引用。 在类中,这些方法是操作序列——如果方法在任何参数和/或全局变量上执行,则参数化操作。
术语“定义”和“模板”以及您使用它们的方式正在造成不必要的混淆。 指令序列是方法定义的一部分。 对象引用这些定义,或者通过已经提到的对类的引用间接引用,或者直接通过方法指针表(称为“vtable”),这是一种广泛的优化。
当其中一个方法,比如说
b.m99()
在运行时被调用时,JRE 转到B
类定义以获取该“模板”(操作序列),查找b
字段的当前值,并填写具有这些字段的当前值的“模板”还将这些当前值推送到堆栈空间,并通过执行在类定义中找到的这些操作来运行方法。
你应该忘记“模板”这个词。 方法定义包含一系列可执行指令,JVM 将执行这些指令。 对于实例方法,指向对象数据的指针成为隐式的第一个参数。 任何模板都不会填充任何东西。
如果该方法是从超类继承的,例如。 上面的
m2()
,类中该方法的定义(上面 A 段中提到的定义)本身就是对类 A 中m2()
定义的引用。在运行时,当
b.m2()
被执行时,JRE 会直接进入A
类以找到要执行的低级操作的“模板”。
这是一个实现细节,但请屏住呼吸……
这些对方法定义的引用在编译时被检查并放入字节码中。 例如。 在字节码为上述的情况下,类
B
有,直接引用方法m2()
类的A
用于方法m2()
它继承A
。
这不是 Java 的工作原理。 在 Java 中,编译器将检查被调用的方法是否存在,当B
从A
继承方法及其可访问性时,编译器会成功,然后,它将记录在源代码中编写的调用,用于在B
调用的m2()
。
这一事实B
继承的方法中,是的实现细节B
并使其变化。 的未来版本B
可覆盖的方法。 如果发生这种情况, A.m2()
甚至可能会被删除。 或者可能会在A
和B
之间引入类C
( C extends A
和B extends C
),它们都是向后兼容的更改。
但是回到上一节,在运行时,实现可能会利用有关实际继承的知识。 每次调用一个方法时,JVM 都可以搜索超类型层次结构,这可能是有效的,但效率不高。
一个不同的策略是拥有上面提到的“vtable”。 每个类在初始化时都会创建一个这样的表,从所有超类方法的副本开始,被覆盖的方法的条目被替换,最后是新声明的方法。
因此,当第一次执行调用指令时,它通过确定 vtable 中的关联索引来链接。 然后,每次调用只需要从对象的实际类的 vtable 中获取方法指针,而无需遍历类层次结构。
这仍然是唯一的方式,解释或优化程度较低的执行工作。 当 JVM 决定进一步优化调用代码时,它可能会预测方法调用的实际目标。 您的示例中有两种方法
JVM 使用A.m2()
从未覆盖的知识A.m2()
当加载新类时它必须删除这样的优化,它确实覆盖了方法)
它分析代码路径,以确定对于B b = new B(); b.m2();
B b = new B(); b.m2();
目标是固定的,因为new B()
的结果总是B
类型,不是超类也不是子类。
当目标被预测时,它可以被内联。 然后,优化后的代码简单地执行System.out.print(new Date());
当B
实例没有其他用途时,甚至分配可能会被消除。
因此,JVM 在运行时所做的可能与源代码中编写的完全不同。 只有可感知的结果(打印日期)是相同的。
方法
m1()
、m2()
和m99()
在b
这个空间中都有它们的定义。
不正确。 为b
分配的空间( B
的实例)引用类本身,即为类B
分配的空间,方法定义存储在其中。
为对象实例分配的空间由对象头和实例的数据(即字段的值)组成。 有关对象头的更多信息,请参见例如java 对象头中的内容。
例如。 在字节码为上述的情况下,类
B
有,直接引用方法m2()
类的A
用于方法m2()
它继承A
。
不正确。 B
类的字节码对方法m2()
一无所知。
请记住,类A
可以与类B
分开编译,因此您可以删除方法m2
而无需重新编译类B
。
更新
来自评论:
那么如何知道在运行
b.m2()
时要执行什么? 我不认为 JRE 进入B
的超类,看起来在那里看到一个m2()
,如果没有这样的方法然后进入超超类,......运行时效率太低。 必须是对m2()
的直接引用。m2()
是B
的成员——即使是继承的。
正如答案中已经说明的那样, m2()
不是B
的成员。 如果您运行 Java 反汇编程序,即在命令行上运行javap B.class
,您将看到:
class B extends A {
int x;
B();
void m1();
void m99();
}
如您所见,编译器为您添加了默认构造函数,但没有添加任何m2()
方法。
现在创建这个类:
class C {
public static void main(String[] args) {
B b = new B();
b.m2();
}
}
然后用-c
开关反汇编它,即javap -c C.class
:
class C {
C();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #16 // class B
3: dup
4: invokespecial #18 // Method B."<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #19 // Method B.m2:()V
12: return
}
如您所见,编译器生成一条指令来调用B.m2()
,即使我们已经看到B.class
不知道m2()
。
这意味着您所假设的正是发生的事情,即 JVM 需要在运行时通过沿超类链向上解析该方法到A
类。
如果从类A
删除m2()
并重新编译,而不重新编译类C
,则在运行代码时会得到NoSuchMethodError: 'void B.m2()'
。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.