根据我之前阅读的内容,在.java
文件编译成.class
个文件后,删除后每个对象都只是Object
。如
Foo f = new Foo();
编译到.class
文件,并反编译,它变为:
Object f = new Foo();
那么JRE在运行时如何调用对象的功能?函数存储在内存中的哪个位置?在对象里面?或者使用类结构的层次结构并查找层次结构?
答案 0 :(得分:4)
Java类文件结构有10个基本部分:
- 幻数:0xCAFEBABE
- 类文件格式的版本:类文件的次要版本和主要版本
- 常量池:类的常量池
- 访问标志:例如,该类是抽象的,静态的等等。
- 此类:当前类的名称
- 超级:超级名称
- 接口:类中的任何接口
- 字段:类中的任何字段
- 方法:类中的任何方法
- 属性:类的任何属性(例如源文件的名称等)
在运行时,检索对象的类型,检查其类文件(或更确切地说是virtual method table)以获取所调用方法的实现。如果该类没有这样的实现,则检查父类(从超类条目中检索),依此类推,如果没有找到则最终失败。
答案 1 :(得分:1)
宣布
时Foo f;
在f
生命周期内的任何时候,它都可以引用不属于Foo
类型的对象。对象类型可以是Foo
或其任何子类。因此,对象必须存储有关对象的实际("运行时")类型的信息,在每个对象中的某个位置。 (我相信这些信息与对象本身相关联,而不是与对象的引用相关联,例如f
。)我不确切地知道这些信息的格式在JVM中是什么样的。但是在我使用的其他编译语言中,类型信息包括指向代码地址向量的指针。如果类型Foo
声明了方法method1()
,method2()
等,那么将为每个方法指定一个索引号(对于在子类中继承或覆盖的方法,将保留该数字) )。所以调用方法意味着转到该向量并找到给定索引的函数地址。无论实际运行时类型是Foo
还是Foo
的任何子类,这都将有效。
答案 2 :(得分:1)
示例代码:
import java.util.*;
public class Foo {
public static void main() {
Foo foo = new Foo();
Object obj = new Object();
foo.f();
ArrayList<Foo> fooList = new ArrayList<Foo>();
ArrayList objList = new ArrayList();
}
public void f() {
}
}
生成的JVM指令( javap -c Foo
):
public class Foo {
public Foo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main();
Code:
0: new #2 // class Foo
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_0
8: new #4 // class java/lang/Object
11: dup
12: invokespecial #1 // Method java/lang/Object."<init>":()V
15: astore_1
16: aload_0
17: invokevirtual #5 // Method f:()V
20: new #6 // class java/util/ArrayList
23: dup
24: invokespecial #7 // Method java/util/ArrayList."<init>":()V
27: astore_2
28: new #6 // class java/util/ArrayList
31: dup
32: invokespecial #7 // Method java/util/ArrayList."<init>":()V
35: astore_3
36: return
public void f();
Code:
0: return
}
如您所见,Foo foo = new Foo();
被翻译为:
0: new #2 // class Foo
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_0
而Object obj = new Object();
变为:
8: new #4 // class java/lang/Object
11: dup
12: invokespecial #1 // Method java/lang/Object."<init>":()V
15: astore_1
new
为对象分配内存并在堆栈中存储引用,dup
在堆栈中创建第二个引用,invokespecial
调用构造函数(实际上是一个名为{的方法{1}})。然后,实例存储在具有<init>
的本地变量中。
至于astore_1
和ArrayList<Foo> fooList = new ArrayList<Foo>();
,它们编译成几乎相同的东西:
ArrayList objList = new ArrayList();
一个使用28: new #6 // class java/util/ArrayList
31: dup
32: invokespecial #7 // Method java/util/ArrayList."<init>":()V
35: astore_3
,另一个使用astore_2
。那是因为它们存储在不同的局部变量中。除此之外,生成的代码是相同的,这意味着JVM无法告诉astore_3
Arraylist<Foo>
,这就是他们所谓的类型擦除。但是,它可以很容易地从Arraylist
告诉Foo
。
答案 3 :(得分:0)
您的示例是local variable。局部变量仅存在于函数内,并且外部世界无法访问。因此,编译器不需要在类文件中存储有关变量的信息(这不是 type erasure,这是指丢失有关参数化类型的信息。)
在类文件中,局部变量在stack frame中被引用为编号的槽。因此,字节码运算符aload_0
检索插槽0中的值(对于实例方法将为this
)并将其置于操作数堆栈的顶部,而astore_1
将关闭引用操作数堆栈的顶部并将其放在帧的第1个插槽中。
由于编译器知道帧中每个槽的类型,因此可以确保只调用正确的方法。 JVM提供了额外的检查:如果您可以修改插槽以包含无效的对象引用,则invokevirtual
操作将失败。
虽然不需要存储有关本地变量的类型信息来运行程序,但是需要将这些信息用于调试。因此,您可以为编译器提供-g
开关,这会导致它在您的类文件中放置LocalVariableTable。该表包含每个局部变量的原始名称和类型。
如果您的反编译器将局部变量显示为Object
,则表示以下两种情况之一:(1)编写没有调试信息的类文件,或(2)反编译器不够智能以应用该信息到字节码。
答案 4 :(得分:-1)
如果您查看VM规范,您将看到每个类的每个方法的代码都存储在类.class文件中。请参阅此链接class file format