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 fooList = new 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文件中。 看到这个链接类文件格式