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