Java如何调用对象的功能?

时间:2014-01-14 18:33:46

标签: java jvm

根据我之前阅读的内容,在.java文件编译成.class个文件后,删除后每个对象都只是Object。如

Foo f = new Foo();

编译到.class文件,并反编译,它变为:

Object f = new Foo();

那么JRE在运行时如何调用对象的功能?函数存储在内存中的哪个位置?在对象里面?或者使用类结构的层次结构并查找层次结构?

5 个答案:

答案 0 :(得分:4)

根据Java specwikipedia

  

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_1ArrayList<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